# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

import copy
import json
import logging
import os
from collections import defaultdict
from pathlib import Path

from lxml import etree
from sql import Literal, Null
from sql.conditionals import Coalesce
from sql.operators import Equal

from trytond.cache import Cache, MemoryCache
from trytond.i18n import gettext
from trytond.model import (
    Exclude, Index, ModelSQL, ModelView, fields, sequence_ordered)
from trytond.model.exceptions import ValidationError
from trytond.pool import Pool
from trytond.pyson import PYSON, Bool, Eval, If, PYSONDecoder
from trytond.rpc import RPC
from trytond.tools import file_open
from trytond.transaction import Transaction
from trytond.wizard import Button, StateView, Wizard

from ..action import DomainError, ViewError

logger = logging.getLogger(__name__)

# Numbers taken from Bootstrap's breakpoints
WIDTH_BREAKPOINTS = [
    1400,
    1200,
    992,
    768,
    576,
    ]


class XMLError(ValidationError):
    pass


class View(
        fields.fmany2one(
            'model_ref', 'model', 'ir.model,name', "Model",
            ondelete='CASCADE'),
        fields.fmany2one(
            'field_children', 'field_childs,model',
            'ir.model.field,name,model',
            "Children Field",
            domain=[
                ('model', '=', Eval('model')),
                ],
            states={
                'invisible': Eval('type') != 'tree',
                }),
        fields.fmany2one(
            'module_ref', 'module', 'ir.module,name', "Module",
            readonly=True, ondelete='CASCADE'),
        sequence_ordered('priority', "Priority"), ModelSQL, ModelView):
    "View"
    __name__ = 'ir.ui.view'
    model = fields.Char('Model', states={
            'required': Eval('type') != 'board',
            })
    type = fields.Selection([
            (None, ''),
            ('tree', 'Tree'),
            ('form', 'Form'),
            ('graph', 'Graph'),
            ('calendar', 'Calendar'),
            ('board', 'Board'),
            ('list-form', "List Form"),
            ], 'View Type',
        domain=[
            If(Bool(Eval('inherit')),
                ('type', '=', None),
                ('type', '!=', None)),
            ],
        depends=['inherit'])
    type_string = type.translated('type')
    data = fields.Text('Data')
    name = fields.Char('Name', states={
            'invisible': ~(Eval('module') & Eval('name')),
            }, depends=['module'], readonly=True)
    arch = fields.Function(fields.Text('View Architecture', states={
                'readonly': Bool(Eval('name')),
                }, depends=['name']), 'get_arch', setter='set_arch')
    basis = fields.Function(fields.Boolean("Basis"), 'get_basis')
    inherit = fields.Many2One('ir.ui.view', 'Inherited View',
            ondelete='CASCADE')
    extensions = fields.One2Many(
        'ir.ui.view', 'inherit', "Extensions",
        filter=[
            ('basis', '=', False),
            ],
        domain=[
            ('model', '=', Eval('model')),
            ('type', '=', None),
            ],
        states={
            'invisible': ~Eval('type'),
            },
        order=[('id', None)])
    field_childs = fields.Char('Children Field', states={
            'invisible': Eval('type') != 'tree',
            }, depends=['type'])
    module = fields.Char('Module', states={
            'invisible': ~Eval('module'),
            }, readonly=True)
    domain = fields.Char('Domain', states={
            'invisible': ~Eval('inherit'),
            }, depends=['inherit'])
    _get_validator_cache = MemoryCache('ir_ui_view.validator', context=False)
    _view_get_cache = Cache('ir_ui_view.view_get')
    __module_index = None

    @classmethod
    def __setup__(cls):
        super().__setup__()
        table = cls.__table__()
        cls.priority.required = True

        cls.__rpc__['view_get'] = RPC(instantiate=0, cache=dict(days=1))
        cls._buttons.update({
                'show': {
                    'readonly': Eval('type') != 'form',
                    'invisible': ~Eval('basis', False),
                    'depends': ['type', 'basis'],
                    },
                })
        cls._sql_indexes.update({
                Index(table,
                    (table.model, Index.Equality()),
                    (table.inherit, Index.Range())),
                Index(
                    table,
                    (table.id, Index.Range()),
                    (table.inherit, Index.Range())),
                })

    @staticmethod
    def default_priority():
        return 16

    @staticmethod
    def default_module():
        return Transaction().context.get('module')

    def get_basis(self, name):
        return not self.inherit or self.model != self.inherit.model

    @classmethod
    def domain_basis(cls, domain, tables):
        table, _ = tables[None]
        if 'inherit' not in tables:
            inherit = cls.__table__()
            tables['inherit'] = {
                None: (inherit, table.inherit == inherit.id),
                }
        else:
            inherit, _ = tables['inherit'][None]
        expression = (table.inherit == Null) | (table.model != inherit.model)

        _, operator, value = domain
        if operator in {'=', '!='}:
            if (operator == '=') != value:
                expression = ~expression
        elif operator in {'in', 'not in'}:
            if True in value and False not in value:
                pass
            elif False in value and True not in value:
                expression = ~expression
            else:
                expression = Literal(True)
        else:
            expression = Literal(True)
        return expression

    def get_rec_name(self, name):
        return ' '.join(filter(None, [
                    self.model_ref.rec_name if self.model_ref else '',
                    '(%s)' % (
                        self.inherit.rec_name if self.inherit else
                        self.type_string),
                    ]))

    @classmethod
    def search_rec_name(cls, name, clause):
        return [('model_ref.rec_name', *clause[1:])]

    @classmethod
    @ModelView.button_action('ir.act_view_show')
    def show(cls, views):
        pass

    def validator(self):
        return self._validator(self.real_type)

    @classmethod
    def _validator(cls, type):
        key = (cls.__name__, type)
        validator = cls._get_validator_cache.get(key)
        if validator is None:
            if type == 'list-form':
                type = 'form'
            filepath = Path(os.path.dirname(__file__), f'{type}.rng')
            if not filepath.exists():
                filepath = filepath.with_suffix('.rnc')
            validator = etree.RelaxNG(file=str(filepath))
            validator = cls._get_validator_cache.set(key, validator)
        return validator

    @property
    def real_type(self):
        if self.inherit:
            return self.inherit.real_type
        return self.type

    @classmethod
    def validate(cls, views):
        super().validate(views)
        cls.check_xml(views)

    @classmethod
    def check_xml(cls, views):
        "Check XML"
        for view in views:
            if not view.arch:
                continue
            xml = view.arch.strip()
            if not xml:
                continue
            tree = etree.fromstring(xml)

            validator = view.validator()
            if not validator.validate(tree):
                error_log = '\n'.join(map(str,
                        validator.error_log.filter_from_errors()))
                raise XMLError(
                    gettext('ir.msg_view_invalid_xml', name=view.rec_name),
                    error_log)
            root_element = tree.getroottree().getroot()

            # validate pyson attributes
            validates = {
                'states': fields.states_validate,
            }

            def encode(element):
                for attr in ('states', 'domain', 'spell'):
                    if not element.get(attr):
                        continue
                    try:
                        value = PYSONDecoder().decode(element.get(attr))
                        validates.get(attr, lambda a: True)(value)
                    except Exception as e:
                        error_log = '%s: <%s %s="%s"/>' % (
                            e, element.get('id') or element.get('name'), attr,
                            element.get(attr))
                        raise XMLError(
                            gettext(
                                'ir.msg_view_invalid_xml', name=view.rec_name),
                            error_log) from e
                for child in element:
                    encode(child)
            encode(root_element)

    def get_arch(self, name):
        pool = Pool()
        value = None
        if self.name and self.module:
            path = os.path.join(self.module, 'view', self.name + '.xml')
            try:
                with file_open(path,
                        subdir='modules', mode='r', encoding='utf-8') as fp:
                    value = fp.read()
            except IOError:
                if pool.test:
                    raise
                logger.exception("failed to open %r", path)
        if not value:
            value = self.data
        return value

    @classmethod
    def set_arch(cls, views, name, value):
        cls.write(views, {'data': value})

    @classmethod
    def on_modification(cls, mode, records, field_names=None):
        super().on_modification(mode, records, field_names=field_names)
        cls._view_get_cache.clear()
        ModelView._fields_view_get_cache.clear()

    @property
    def _module_index(self):
        from trytond.modules import create_graph, get_modules
        if self.__class__.__module_index is None:
            graph = create_graph(get_modules(with_test=Pool.test))
            modules = [m.name for m in graph]
            self.__class__.__module_index = {
                m: i for i, m in enumerate(reversed(modules))}
        return self.__class__.__module_index

    def view_get(self, model=None):
        key = (self.id, model)
        result = self._view_get_cache.get(key)
        if result:
            return result.copy()
        if self.inherit:
            if self.inherit.model == model:
                return self.inherit.view_get(model=model)
            else:
                arch = self.inherit.view_get(self.inherit.model)['arch']
        else:
            arch = self.arch

        views = self.__class__.search(['OR', [
                    ('inherit', '=', self.id),
                    ('model', '=', model),
                    ], [
                    ('id', '=', self.id),
                    ('inherit', '!=', None),
                    ],
                ])
        views.sort(
            key=lambda v: self._module_index.get(v.module, -1), reverse=True)
        parser = etree.XMLParser(remove_comments=True, resolve_entities=False)
        tree = etree.fromstring(arch, parser=parser)
        decoder = PYSONDecoder({'context': Transaction().context})
        for view in views:
            if view.domain and not decoder.decode(view.domain):
                continue
            if not view.arch or not view.arch.strip():
                continue
            tree_inherit = etree.fromstring(view.arch, parser=parser)
            tree = self.inherit_apply(tree, tree_inherit)
        if model:
            root = tree.getroottree().getroot()
            self._translate(root, model, Transaction().language)
        arch = etree.tostring(tree, encoding='utf-8').decode('utf-8')
        result = {
            'type': self.real_type,
            'view_id': self.id,
            'arch': arch,
            'field_childs': self.field_childs,
            }
        return self._view_get_cache.set(key, result)

    @classmethod
    def inherit_apply(cls, tree, inherit):
        root_inherit = inherit.getroottree().getroot()
        for element in root_inherit:
            expr = element.get('expr')
            targets = tree.xpath(expr)
            assert targets, "No elements found for expression %r" % expr
            for target in targets:
                position = element.get('position', 'inside')
                new_tree = getattr(cls, '_inherit_apply_%s' % position)(
                    tree, element, target)
                if new_tree is not None:
                    tree = new_tree
        return tree

    @classmethod
    def _inherit_apply_replace(cls, tree, element, target):
        parent = target.getparent()
        if parent is None:
            tree, = element
            return tree
        cls._inherit_apply_after(tree, element, target)
        parent.remove(target)

    @classmethod
    def _inherit_apply_replace_attributes(cls, tree, element, target):
        child, = element
        for attr in child.attrib:
            target.set(attr, child.get(attr))

    @classmethod
    def _inherit_apply_inside(cls, tree, element, target):
        target.extend(list(element))

    @classmethod
    def _inherit_apply_after(cls, tree, element, target):
        parent = target.getparent()
        next_ = target.getnext()
        if next_ is not None:
            for child in element:
                index = parent.index(next_)
                parent.insert(index, child)
        else:
            parent.extend(list(element))

    @classmethod
    def _inherit_apply_before(cls, tree, element, target):
        parent = target.getparent()
        for child in element:
            index = parent.index(target)
            parent.insert(index, child)

    @classmethod
    def _translate(cls, element, model, language):
        pool = Pool()
        Translation = pool.get('ir.translation')
        for attr in ['string', 'sum', 'confirm', 'help']:
            if element.get(attr):
                translation = Translation.get_source(
                    model, 'view', language, element.get(attr))
                if translation:
                    element.set(attr, translation)
        for child in element:
            cls._translate(child, model, language)


class ShowViewStart(ModelView):
    'Show view'
    __name__ = 'ir.ui.view.show.start'
    __no_slots__ = True


class ShowView(Wizard):
    'Show view'
    __name__ = 'ir.ui.view.show'

    class ShowStateView(StateView):

        def __init__(self, model_name, buttons):
            StateView.__init__(self, model_name, None, buttons)

        def get_view(self, wizard, state_name):
            pool = Pool()
            View = pool.get('ir.ui.view')
            view_id = Transaction().context.get('active_id')
            if not view_id:
                # Set type to please ModuleTestCase.test_wizards
                return {'type': 'form'}
            view = View(view_id)
            Model = pool.get(view.model)
            return Model.fields_view_get(view_id=view.id)

        def get_defaults(self, wizard, state_name, fields):
            return {}

    start = ShowStateView('ir.ui.view.show.start', [
            Button('Close', 'end', 'tryton-close', default=True),
            ])


class ViewTreeWidth(
        fields.fmany2one(
            'model_ref', 'model', 'ir.model,name', "Model",
            required=True, ondelete='CASCADE'),
        fields.fmany2one(
            'field_ref', 'field,model', 'ir.model.field,name,model', "Field",
            required=True, ondelete='CASCADE',
            domain=[
                ('model', '=', Eval('model')),
                ]),
        ModelSQL, ModelView):
    "View Tree Width"
    __name__ = 'ir.ui.view_tree_width'
    model = fields.Char('Model', required=True)
    field = fields.Char('Field', required=True)
    occurrence = fields.Integer("Occurrence", required=True)
    user = fields.Many2One('res.user', 'User', required=True,
        ondelete='CASCADE')
    screen_width = fields.Integer(
        "Screen Width",
        domain=[
            ('screen_width', '>=', 0),
            ])
    width = fields.Integer(
        "Width",
        domain=['OR',
            ('width', '=', None),
            ('width', '>=', 0),
            ])

    @classmethod
    def __setup__(cls):
        super().__setup__()
        table = cls.__table__()
        cls.__rpc__.update({
                'set_width': RPC(readonly=False),
                'reset_width': RPC(readonly=False),
                })
        cls._sql_constraints += [
            ('field_occurrence_user_unique',
                Exclude(table,
                    (table.model, Equal),
                    (table.field, Equal),
                    (table.occurrence, Equal),
                    (table.user, Equal),
                    (Coalesce(table.screen_width, -1), Equal)),
                'ir.msg_view_tree_width_field_occurrence_user_unique'),
            ]
        cls._sql_indexes.add(
            Index(
                table,
                (table.user, Index.Range()),
                (table.model, Index.Equality()),
                (table.field, Index.Equality()),
                (table.occurrence, Index.Equality())))

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        super().__register__(module)

        # Migration from 7.8: remove required on width
        table_h.not_null_action('width', 'remove')

    @classmethod
    def default_occurrence(cls):
        return 1

    def get_rec_name(self, name):
        return f'{self.field_ref.rec_name} @ {self.model_ref.rec_name}'

    @classmethod
    def search_rec_name(cls, name, clause):
        operator = clause[1]
        if operator.startswith('!') or operator.startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
            ('model_ref.rec_name', *clause[1:]),
            ('field_ref.rec_name', *clause[1:]),
            ]

    @classmethod
    def on_modification(cls, mode, records, field_names=None):
        super().on_modification(mode, records, field_names=field_names)
        ModelView._fields_view_get_cache.clear()

    @classmethod
    def get_width(cls, model, width):
        for screen_width in WIDTH_BREAKPOINTS:
            if width >= screen_width:
                break
        else:
            screen_width = None

        user = Transaction().user
        records = cls.search([
            ('user', '=', user),
            ('model', '=', model),
            ('screen_width', '=', screen_width),
            ],
            order=[('occurrence', 'ASC')])

        if not records:
            records = cls.search([
                ('user', '=', user),
                ('model', '=', model),
                ['OR',
                    ('screen_width', '<=', screen_width),
                    ('screen_width', '=', None),
                    ],
                ],
                order=[
                    ('screen_width', 'DESC NULLS LAST'),
                    ('occurrence', 'ASC'),
                    ])
        screen_width = None
        widths = defaultdict(list)
        for width in records:
            if screen_width is None:
                screen_width = width.screen_width
            if screen_width != width.screen_width:
                break
            if len(widths[width.field]) + 1 < width.occurrence:
                for _ in range(
                        width.occurrence - len(widths[width.field]) - 1):
                    widths[width.field].append(None)
            widths[width.field].insert(width.occurrence, width.width)
        return widths

    @classmethod
    def set_width(cls, model, fields, width):
        '''
        Set width for the current user on the model.
        fields is dictionary with field name as key and a list of widths
        as value.
        width is the screen width.
        '''
        for screen_width in WIDTH_BREAKPOINTS:
            if width >= screen_width:
                break
        else:
            screen_width = None

        user_id = Transaction().user
        records = cls.search([
                ('user', '=', user_id),
                ('model', '=', model),
                ('field', 'in', list(fields.keys())),
                ('screen_width', '=', screen_width),
                ],
            order=[('occurrence', 'DESC')])

        fields = copy.deepcopy(fields)
        to_save = []
        for tree_width in records:
            if tree_width.screen_width == screen_width:
                index = tree_width.occurrence - 1
                if index <= len(fields[tree_width.field]):
                    width = fields[tree_width.field][index]
                    fields[tree_width.field][index] = None
                    if width is not None:
                        tree_width.width = width
                        to_save.append(tree_width)

        for name, widths in fields.items():
            for occurrence, width in enumerate(widths, start=1):
                if width is not None:
                    to_save.append(cls(
                            user=user_id,
                            model=model,
                            field=name,
                            occurrence=occurrence,
                            screen_width=screen_width,
                            width=width))

        if to_save:
            cls.save(to_save)

    @classmethod
    def reset_width(cls, model, width):
        for screen_width in WIDTH_BREAKPOINTS:
            if width >= screen_width:
                break
        else:
            screen_width = None

        user_id = Transaction().user
        records = cls.search([
                ('user', '=', user_id),
                ('model', '=', model),
                ['OR',
                    ('screen_width', '=', screen_width),
                    ('screen_width', '=', None),
                    ],
                ])
        cls.delete(records)


class ViewTreeOptional(
        fields.fmany2one(
            'model_ref', 'model', 'ir.model,name', "Model",
            required=True, ondelete='CASCADE'),
        fields.fmany2one(
            'field_ref', 'field,model', 'ir.model.field,name,model', "Field",
            required=True, ondelete='CASCADE',
            domain=[
                ('model', '=', Eval('model')),
                ]),
        ModelSQL, ModelView):
    "View Tree Optional"
    __name__ = 'ir.ui.view_tree_optional'
    view = fields.Many2One(
        'ir.ui.view', "View", required=True, ondelete='CASCADE')
    user = fields.Many2One(
        'res.user', "User", required=True, ondelete='CASCADE')
    model = fields.Char("Model", required=True)
    field = fields.Char("Field", required=True)
    value = fields.Boolean("Value")

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__rpc__.update({
                'set_optional': RPC(readonly=False),
                })
        table = cls.__table__()
        cls._sql_indexes.add(
            Index(
                table,
                (table.user, Index.Range()),
                (table.view, Index.Range())))

    @classmethod
    def __register__(cls, module):
        pool = Pool()
        View = pool.get('ir.ui.view')
        table = cls.__table__()
        view = View.__table__()
        table_h = cls.__table_handler__(module)
        cursor = Transaction().connection.cursor()

        # Migration from 7.2: rename view_id into view
        table_h.column_rename('view_id', 'view')

        super().__register__(module)

        # Migration from 7.2: add model
        cursor.execute(*table.update(
                [table.model],
                [view.select(view.model, where=view.id == table.view)],
                where=table.model == Null))

    @classmethod
    def validate_fields(cls, records, fields_names):
        super().validate_fields(records, fields_names)
        cls.check_view(records, fields_names)

    @classmethod
    def check_view(cls, records, fields_names=None):
        if fields_names and 'view' not in fields_names:
            return
        for record in records:
            if record.view and record.view.real_type != 'tree':
                raise ViewError(gettext(
                        'ir.msg_view_tree_optional_type',
                        view=record.view.rec_name))

    @classmethod
    def on_modification(cls, mode, record, field_names=None):
        super().on_modification(mode, record, field_names=field_names)
        ModelView._fields_view_get_cache.clear()

    @classmethod
    def set_optional(cls, view_id, fields):
        "Store optional field that must be displayed"
        pool = Pool()
        View = pool.get('ir.ui.view')
        user = Transaction().user
        view = View(view_id)
        records = cls.search([
                ('view', '=', view.id),
                ('user', '=', user),
                ('field', 'in', list(fields)),
                ])
        cls.delete(records)
        to_create = []
        for field, value in fields.items():
            to_create.append({
                    'view': view,
                    'user': user,
                    'model': view.model,
                    'field': field,
                    'value': bool(value),
                    })
        if to_create:
            cls.create(to_create)


class ViewTreeState(
        fields.fmany2one(
            'model_ref', 'model', 'ir.model,name', "Model",
            required=True, ondelete='CASCADE'),
        fields.fmany2one(
            'child_field', 'child_name,model', 'ir.model.field,name,model',
            "Child Field", ondelete='CASCADE',
            domain=[
                ('model', '=', Eval('model')),
                ]),
        ModelSQL, ModelView):
    'View Tree State'
    __name__ = 'ir.ui.view_tree_state'
    _rec_name = 'model'
    model = fields.Char('Model', required=True)
    domain = fields.Char('Domain', required=True)
    user = fields.Many2One('res.user', 'User', required=True,
            ondelete='CASCADE')
    child_name = fields.Char('Child Name')
    nodes = fields.Text('Expanded Nodes')
    selected_nodes = fields.Text('Selected Nodes')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__rpc__.update({
                'set': RPC(readonly=False, check_access=False),
                'get': RPC(check_access=False, cache=dict(days=1)),
                })

        table = cls.__table__()
        cls._sql_indexes.add(
            Index(
                table,
                (table.user, Index.Range()),
                (table.model, Index.Equality()),
                (table.child_name, Index.Equality()),
                (table.domain, Index.Equality())))

    @staticmethod
    def default_nodes():
        return '[]'

    @staticmethod
    def default_selected_nodes():
        return '[]'

    @classmethod
    def set(cls, model, domain, child_name, nodes, selected_nodes):
        # Normalize the json domain
        domain = json.dumps(json.loads(domain), separators=(',', ':'))
        current_user = Transaction().user
        records = cls.search([
                ('user', '=', current_user),
                ('model', '=', model),
                ('domain', '=', domain),
                ('child_name', '=', child_name),
                ])
        cls.delete(records)
        cls.create([{
                    'user': current_user,
                    'model': model,
                    'domain': domain,
                    'child_name': child_name,
                    'nodes': nodes,
                    'selected_nodes': selected_nodes,
                    }])

    @classmethod
    def get(cls, model, domain, child_name):
        # Normalize the json domain
        domain = json.dumps(json.loads(domain), separators=(',', ':'))
        current_user = Transaction().user
        try:
            expanded_info, = cls.search([
                    ('user', '=', current_user),
                    ('model', '=', model),
                    ('domain', '=', domain),
                    ('child_name', '=', child_name),
                    ],
                limit=1)
        except ValueError:
            return (cls.default_nodes(), cls.default_selected_nodes())
        state = cls(expanded_info)
        return (state.nodes or cls.default_nodes(),
            state.selected_nodes or cls.default_selected_nodes())


class ViewSearch(
        fields.fmany2one(
            'model_ref', 'model', 'ir.model,name', "Model",
            required=True, ondelete='CASCADE'),
        ModelSQL, ModelView):
    "View Search"
    __name__ = 'ir.ui.view_search'

    name = fields.Char('Name', required=True)
    model = fields.Char('Model', required=True)
    domain = fields.Char('Domain', help="The PYSON domain.")
    user = fields.Many2One('res.user', 'User', ondelete='CASCADE')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__rpc__.update({
                'get': RPC(check_access=False),
                'set': RPC(check_access=False, readonly=False),
                'unset': RPC(check_access=False, readonly=False),
                })

    @staticmethod
    def default_user():
        return Transaction().user

    @classmethod
    def validate_fields(cls, searches, field_names):
        super().validate_fields(searches, field_names)
        cls.check_domain(searches, field_names)

    @classmethod
    def check_domain(cls, searches, field_names):
        decoder = PYSONDecoder()
        if field_names and 'domain' not in field_names:
            return
        for search in searches:
            try:
                value = decoder.decode(search.domain)
            except Exception as exception:
                raise DomainError(
                    gettext('ir.msg_view_search_invalid_domain',
                        domain=search.domain,
                        search=search.rec_name)) from exception
            if isinstance(value, PYSON):
                if not value.types() == set([list]):
                    raise DomainError(
                        gettext('ir.msg_view_search_invalid_domain',
                            domain=search.domain,
                            search=search.rec_name))
            elif not isinstance(value, list):
                raise DomainError(
                    gettext('ir.msg_view_search_invalid_domain',
                        domain=search.domain,
                        search=search.rec_name))
            else:
                try:
                    fields.domain_validate(value)
                except Exception as exception:
                    raise DomainError(
                        gettext('ir.msg_view_search_invalid_domain',
                            domain=search.domain,
                            search=search.rec_name)) from exception

    @classmethod
    def get(cls):
        decoder = PYSONDecoder()
        user = Transaction().user
        searches = cls.search_read(['OR',
                ('user', '=', user),
                ('user', '=', None),
                ],
            order=[('model', 'ASC'), ('name', 'ASC')],
            fields_names=['id', 'name', 'model', 'domain', '_delete'])
        result = {}
        for search in searches:
            result.setdefault(search['model'], []).append((
                    search['id'],
                    search['name'],
                    decoder.decode(search['domain']),
                    search['_delete']))
        return result

    @classmethod
    def set(cls, name, model, domain):
        user = Transaction().user
        search, = cls.create([{
                    'name': name,
                    'model': model,
                    'domain': domain,
                    'user': user,
                    }])
        return search.id

    @classmethod
    def unset(cls, id):
        user = Transaction().user
        cls.delete(cls.search([
                    ('id', '=', id),
                    ('user', '=', user),
                    ]))
