# 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 math
import operator

from gi.repository import Gdk, GLib, GObject, Gtk

from tryton.common import RPCException, RPCExecute, eval_domain


class SelectionMixin(object):

    def __init__(self, *args, **kwargs):
        super(SelectionMixin, self).__init__(*args, **kwargs)
        self.nullable_widget = True
        self.selection = None
        self.inactive_selection = []
        self._last_domain = None
        self._values2selection = {}
        self._domain_cache = {}

    def init_selection(self, value=None):
        if value is None:
            value = dict((k, None)
                for k in self.attrs.get('selection_change_with') or [])
        key = freeze_value(value)
        selection = self.attrs.get('selection', [])[:]
        help_ = self.attrs.get('help_selection', {})
        if (not isinstance(selection, (list, tuple))
                and key not in self._values2selection):
            try:
                if self.attrs.get('selection_change_with'):
                    selection = RPCExecute('model', self.model_name, selection,
                        value)
                else:
                    selection = RPCExecute('model', self.model_name, selection)
            except RPCException:
                selection = []
            self._values2selection[key] = selection
        elif key in self._values2selection:
            selection = self._values2selection[key]
        if self.attrs.get('sort', True):
            selection.sort(key=operator.itemgetter(1))
        self.selection = selection[:]
        self.help = help_
        self.inactive_selection = []

    def update_selection(self, record, field):
        if not field:
            return
        if not self.selection:
            self.init_selection()

        domain = field.domain_get(record)
        if 'relation' not in self.attrs:
            change_with = self.attrs.get('selection_change_with') or []
            value = record._get_on_change_args(change_with)
            value.pop('id', None)
            self.init_selection(value)
            self.filter_selection(domain, record, field)
        else:
            context = field.get_context(record)
            domain_cache_key = (freeze_value(domain), freeze_value(context))
            if domain_cache_key in self._domain_cache:
                self.selection = self._domain_cache[domain_cache_key]
                self._last_domain = (domain, context)
            if (domain, context) == self._last_domain:
                return
            fields = ['rec_name']
            help_field = self.attrs.get('help_field')
            if help_field:
                fields.append(help_field)
            try:
                result = RPCExecute('model', self.attrs['relation'],
                    'search_read', domain, 0, None, None, fields,
                    context=context, process_exception=False)
            except RPCException:
                result = False
            if isinstance(result, list):
                selection = [(x['id'], x['rec_name']) for x in result]
                if self.nullable_widget:
                    selection.append((None, ''))
                if help_field:
                    help_ = {x['id']: x[help_field] for x in result}
                else:
                    help_ = {}
                self._last_domain = (domain, context)
                self._domain_cache[domain_cache_key] = selection
            else:
                selection = []
                if self.nullable_widget:
                    selection.append((None, ''))
                help_ = {}
                self._last_domain = None
            self.selection = selection[:]
            self.help = help_
            self.inactive_selection = []

    def filter_selection(self, domain, record, field):
        if not domain:
            return

        def _value_evaluator(value):
            return eval_domain(domain, {
                    self.field_name: value[0],
                    })

        def _model_evaluator(allowed_models):
            def test(value):
                return value[0] in allowed_models or not allowed_models
            return test

        type_ = field.attrs['type']
        if type_ == 'reference':
            allowed_models = field.get_models(record)
            evaluator = _model_evaluator(allowed_models)
        elif type_ == 'multiselection':
            return
        else:
            evaluator = _value_evaluator
        self.selection = list(filter(evaluator, self.selection))

    def get_inactive_selection(self, value):
        if 'relation' not in self.attrs:
            return ''
        if value is None:
            return ''
        for val, text in self.inactive_selection:
            if str(val) == str(value):
                return text
        else:
            try:
                result, = RPCExecute('model', self.attrs['relation'], 'read',
                    [value], ['rec_name'])
                self.inactive_selection.append((result['id'],
                        result['rec_name']))
                return result['rec_name']
            except RPCException:
                return ''


def selection_shortcuts(entry):
    def key_press(widget, event):
        if (event.type == Gdk.EventType.KEY_PRESS
                and event.state & Gdk.ModifierType.CONTROL_MASK
                and event.keyval == Gdk.KEY_space):
            widget.popup()
    entry.connect('key_press_event', key_press)
    return entry


def freeze_value(value):
    if isinstance(value, dict):
        return tuple(sorted((k, freeze_value(v))
                for k, v in value.items()))
    elif isinstance(value, (list, set, tuple)):
        return tuple(freeze_value(v) for v in value)
    else:
        return value


class PopdownMixin(object):

    def set_popdown(self, selection, entry):
        child = entry.get_child()
        if not child:  # entry is destroyed
            return
        model, lengths = self.get_popdown_model(selection)
        entry.set_model(model)
        entry.set_entry_text_column(0)
        completion = Gtk.EntryCompletion()
        completion.set_inline_selection(True)
        completion.set_model(model)
        child.set_completion(completion)
        if lengths:
            pop = sorted(lengths, reverse=True)
            average = sum(pop) / len(pop)
            deviation = int(
                math.sqrt(sum((x - average) ** 2 for x in pop)
                    / len(pop)))
            width = max(next(
                    (x for x in pop if abs(x - average) < (deviation * 2)),
                    10),
                10)
        else:
            width = 10
        child.set_width_chars(width)
        if lengths:
            child.set_max_length(max(lengths))
        completion.set_text_column(0)
        completion.connect('match-selected', self.match_selected, entry)

    def get_popdown_model(self, selection):
        model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_PYOBJECT)
        lengths = []
        for (value, name) in selection:
            name = str(name)
            model.append((name, value))
            lengths.append(len(name))
        return model, lengths

    def match_selected(self, completion, model, iter_, entry):
        value, = model.get(iter_, 1)
        model = entry.get_model()
        for i, values in enumerate(model):
            if values[1] == value:
                GLib.idle_add(entry.set_active, i)
                break

    def get_popdown_value(self, entry, index=1):
        active = entry.get_active()
        if active < 0:
            return None
        else:
            model = entry.get_model()
            return model[active][index]

    def get_popdown_text(self, entry):
        return self.get_popdown_value(entry, index=0)

    def set_popdown_value(self, entry, value):
        active = -1
        model = entry.get_model()
        for i, selection in enumerate(model):
            if selection[1] == value:
                active = i
                break
        else:
            if value:
                return False
        entry.set_active(active)
        if active == -1:
            # When setting no item GTK doesn't clear the entry
            entry.get_child().set_text('')
        return True
