# This file is part of Tryton.  The COPYRIGHT file at the toplevel of this
# repository contains the full copyright notices and license terms.
import json
from collections import OrderedDict

import trytond.config as config
from trytond.cache import Cache
from trytond.i18n import gettext, lazy_gettext
from trytond.model import fields
from trytond.model.exceptions import ValidationError
from trytond.pool import Pool
from trytond.pyson import Eval, PYSONDecoder
from trytond.rpc import RPC
from trytond.tools import slugify
from trytond.transaction import Transaction, inactive_records


class DomainError(ValidationError):
    pass


class SelectionError(ValidationError):
    pass


class DictSchemaMixin(object):
    __slots__ = ()
    _rec_name = 'string'
    name = fields.Char(lazy_gettext('ir.msg_dict_schema_name'), required=True)
    string = fields.Char(
        lazy_gettext('ir.msg_dict_schema_string'),
        translate=True, required=True)
    help = fields.Text(
        lazy_gettext('ir.msg_dict_schema_help'),
        translate=True)
    type_ = fields.Selection([
            ('boolean', lazy_gettext('ir.msg_dict_schema_boolean')),
            ('integer', lazy_gettext('ir.msg_dict_schema_integer')),
            ('char', lazy_gettext('ir.msg_dict_schema_char')),
            ('color', lazy_gettext('ir.msg_dict_schema_color')),
            ('float', lazy_gettext('ir.msg_dict_schema_float')),
            ('numeric', lazy_gettext('ir.msg_dict_schema_numeric')),
            ('date', lazy_gettext('ir.msg_dict_schema_date')),
            ('datetime', lazy_gettext('ir.msg_dict_schema_datetime')),
            ('selection', lazy_gettext('ir.msg_dict_schema_selection')),
            ('multiselection',
                lazy_gettext('ir.msg_dict_schema_multiselection')),
            ], lazy_gettext('ir.msg_dict_schema_type'), required=True)
    digits = fields.Integer(
        lazy_gettext('ir.msg_dict_schema_digits'),
        states={
            'invisible': ~Eval('type_').in_(['float', 'numeric']),
            }, depends=['type_'])
    domain = fields.Char(lazy_gettext('ir.msg_dict_schema_domain'))
    selection = fields.Text(
        lazy_gettext('ir.msg_dict_schema_selection'),
        states={
            'invisible': ~Eval('type_').in_(['selection', 'multiselection']),
            }, translate=True, depends=['type_'],
        help=lazy_gettext('ir.msg_dict_schema_selection_help'))
    selection_sorted = fields.Boolean(
        lazy_gettext('ir.msg_dict_schema_selection_sorted'),
        states={
            'invisible': ~Eval('type_').in_(['selection', 'multiselection']),
            }, depends=['type_'],
        help=lazy_gettext('ir.msg_dict_schema_selection_sorted_help'))
    help_selection = fields.Text(
        lazy_gettext('ir.msg_dict_schema_help_selection'), translate=True,
        states={
            'invisible': ~Eval('type_').in_(['selection', 'multiselection']),
            },
        depends=['type_'],
        help=lazy_gettext('ir.msg_dict_schema_help_selection_help'))
    selection_json = fields.Function(fields.Char(
            lazy_gettext('ir.msg_dict_schema_selection_json'),
            states={
                'invisible': ~Eval('type_').in_(
                    ['selection', 'multiselection']),
                },
            depends=['type_']), 'get_selection_json')
    help_selection_json = fields.Function(fields.Char(
            lazy_gettext('ir.msg_dict_schema_help_selection_json'),
            states={
                'invisible': ~Eval('type_').in_(
                    ['selection', 'multiselection']),
                },
            depends=['type_']), 'get_selection_json')
    _relation_fields_cache = Cache('_dict_schema_mixin.get_relation_fields')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__rpc__.update({
                'get_keys': RPC(
                    instantiate=0,
                    size_limits={
                        0: config.getint('request', 'records_limit'),
                        }),
                'search_get_keys': RPC(
                    size_limits={
                        1: config.getint('request', 'records_limit'),
                        },
                    timeout=config.getint('request', 'timeout', default=0)),
                })

    @staticmethod
    def default_digits():
        return 2

    @staticmethod
    def default_selection_sorted():
        return True

    @fields.depends('name', 'string')
    def on_change_string(self):
        if not self.name and self.string:
            self.name = slugify(self.string.lower(), hyphenate='_')

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

    @classmethod
    def check_domain(cls, schemas, field_names=None):
        if field_names and 'domain' not in field_names:
            return
        for schema in schemas:
            if not schema.domain:
                continue
            try:
                value = PYSONDecoder().decode(schema.domain)
            except Exception:
                raise DomainError(
                    gettext('ir.msg_dict_schema_invalid_domain',
                        schema=schema.rec_name))
            if not isinstance(value, list):
                raise DomainError(
                    gettext('ir.msg_dict_schema_invalid_domain',
                        schema=schema.rec_name))

    @classmethod
    def check_selection(cls, schemas, field_names=None):
        if field_names and not (field_names & {
                    'type_', 'selection', 'help_selection'}):
            return
        for schema in schemas:
            if schema.type_ not in {'selection', 'multiselection'}:
                continue
            for name in ['selection', 'help_selection']:
                try:
                    dict(json.loads(schema.get_selection_json(name + '_json')))
                except Exception:
                    raise SelectionError(
                        gettext('ir.msg_dict_schema_invalid_%s' % name,
                            schema=schema.rec_name))

    def get_selection_json(self, name):
        field = name[:-len('_json')]
        db_selection = getattr(self, field) or ''
        selection = [[w.strip() for w in v.split(':', 1)]
            for v in db_selection.splitlines() if v]
        return json.dumps(selection, separators=(',', ':'))

    @classmethod
    def get_keys(cls, records):
        pool = Pool()
        Config = pool.get('ir.configuration')
        keys = []
        for record in records:
            new_key = {
                'id': record.id,
                'name': record.name,
                'string': record.string,
                'help': record.help,
                'type': record.type_,
                'domain': record.domain,
                'sequence': getattr(record, 'sequence', record.name),
                }
            if record.type_ in {'selection', 'multiselection'}:
                with Transaction().set_context(language=Config.get_language()):
                    english_key = cls(record.id)
                    selection = OrderedDict(json.loads(
                            english_key.selection_json))
                selection.update(dict(json.loads(record.selection_json)))
                new_key['selection'] = list(selection.items())
                new_key['help_selection'] = dict(
                    json.loads(record.help_selection_json))
                new_key['sort'] = record.selection_sorted
            elif record.type_ in ('float', 'numeric'):
                new_key['digits'] = (16, record.digits)
            keys.append(new_key)
        return keys

    @classmethod
    def search_get_keys(cls, domain, limit=None):
        with inactive_records():
            schemas = cls.search(domain, limit=limit)
        return cls.get_keys(schemas)

    @classmethod
    def get_relation_fields(cls):
        if not config.get('dict', cls.__name__, default=True):
            return {}
        fields = cls._relation_fields_cache.get(cls.__name__)
        if fields is not None:
            return fields
        keys = cls.get_keys(cls.search([]))
        fields = {k['name']: k for k in keys}
        return cls._relation_fields_cache.set(cls.__name__, fields)

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

    def format(self, value, lang=None):
        pool = Pool()
        Lang = pool.get('ir.lang')
        if lang is None:
            lang = Lang.get()
        if value is None:
            return ''
        if self.type_ == 'boolean':
            if value:
                return gettext('ir.msg_dict_yes')
            else:
                return gettext('ir.msg_dict_no')
        elif self.type_ == 'integer':
            return lang.format('%i', value)
        elif self.type_ in {'float', 'numeric'}:
            return lang.format('%.*f', (self.digits, value))
        elif self.type_ in {'date', 'datetime'}:
            return lang.strftime(value)
        elif self.type_ in {'selection', 'multiselection'}:
            values = dict(json.loads(self.selection_json))
            if self.type_ == 'selection':
                return values.get(value, '')
            else:
                return "; ".join(values.get(v, '') for v in value)
        return value
