# 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 datetime
import gettext
import logging
import os
import webbrowser
from functools import partial, wraps
from weakref import WeakKeyDictionary

from gi.repository import Gdk, GLib, Gtk

import tryton.common as common
from tryton.common import (
    data2pixbuf, file_open, file_selection, file_write, get_gdk_backend)
from tryton.common.cellrendererbutton import CellRendererButton
from tryton.common.cellrendererclickablepixbuf import (
    CellRendererClickablePixbuf)
from tryton.common.cellrenderercombo import CellRendererCombo
from tryton.common.cellrendererfloat import CellRendererFloat
from tryton.common.cellrendererinteger import CellRendererInteger
from tryton.common.cellrenderertext import (
    CellRendererText, CellRendererTextCompletion)
from tryton.common.cellrenderertoggle import CellRendererToggle
from tryton.common.completion import get_completion, update_completion
from tryton.common.datetime_ import (
    CellRendererDate, CellRendererTime, date_parse)
from tryton.common.domain_parser import quote
from tryton.common.selection import (
    PopdownMixin, SelectionMixin, selection_shortcuts)
from tryton.config import CONFIG
from tryton.gui.window.view_form.screen import Screen
from tryton.gui.window.win_form import WinForm
from tryton.gui.window.win_search import WinSearch

_ = gettext.gettext
logger = logging.getLogger(__name__)

COLORS = {n: v for n, v in zip(
        ['muted', 'success', 'warning', 'danger'],
        CONFIG['tree.colors'].split(','))}


def send_keys(renderer, editable, position, treeview):
    editable.connect('key_press_event', treeview.on_keypressed, renderer)
    editable.editing_done_id = editable.connect('editing_done',
            treeview.on_editing_done, renderer)
    if isinstance(editable, Gtk.ComboBox):
        def changed(combobox):
            # "changed" signal is also triggered by text editing
            # so only trigger editing-done if a row is active
            if combobox.get_active_iter():
                treeview.on_editing_done(combobox, renderer)
        editable.connect('changed', changed)


def catch_errors(error_value=_('#ERROR')):
    def decorator(func):
        @wraps(func)
        def wrapper(self, record):
            if record.exception:
                return error_value
            try:
                return func(self, record)
            except Exception:
                logger.error(
                    f"Error calling {func.__name__} for {self} with {record}",
                    exc_info=True)
                return error_value
        return wrapper
    return decorator


def realized(func):

    @wraps(func)
    def wrapper(self, *args, **kwargs):
        has_been_realized = _REALIZED.get(self.view.treeview, False)
        if not has_been_realized:
            has_been_realized = self.view.treeview.get_realized()
            if has_been_realized:
                self.view.treeview.queue_resize()
                _REALIZED[self.view.treeview] = True
            else:
                return
        return func(self, *args, **kwargs)
    return wrapper


_REALIZED = WeakKeyDictionary()


class CellCache(list):

    methods = ('set_active', 'set_sensitive', 'set_property')

    def apply(self, cell):
        for method, args, kwargs in self:
            getattr(cell, method)(*args, **kwargs)

    def decorate(self, cell):
        def decorate(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                self.append((func.__name__, args, kwargs))
                return func(*args, **kwargs)
            wrapper.previous = func
            return wrapper

        for method in self.methods:
            if getattr(cell, method, None):
                setattr(cell, method, decorate(getattr(cell, method)))
        cell.decorated = True
        return cell

    def undecorate(self, cell):
        for method in self.methods:
            if getattr(cell, method, None):
                setattr(cell, method, getattr(cell, method).previous)
        del cell.decorated

    @classmethod
    def cache(cls, func):
        @wraps(func)
        def wrapper(self, column, cell, store, iter_, user_data=None):
            if not hasattr(self, 'display_counters'):
                self.display_counters = {}
            if not hasattr(self, 'cell_caches'):
                self.cell_caches = {}
            record = store.get_value(iter_, 0)
            counter = self.view.treeview.display_counter
            if (self.display_counters.get(record.id) != counter):
                if getattr(cell, 'decorated', None) or record.exception:
                    func(self, column, cell, store, iter_, user_data)
                else:
                    cache = cls()
                    cache.decorate(cell)
                    func(self, column, cell, store, iter_, user_data)
                    cache.undecorate(cell)
                    self.cell_caches[record.id] = cache
                    self.display_counters[record.id] = counter
            else:
                self.cell_caches[record.id].apply(cell)
        return wrapper


class Cell(object):
    renderer = None
    setter = None
    expand = True
    attrs = None
    view = None
    prefixes = []
    suffixes = []

    def _get_record_field_from_path(self, path, store=None):
        if not store:
            store = self.view.treeview.get_model()
        record = store.get_value(store.get_iter(path), 0)
        field = record.group.fields[self.attrs['name']]
        return record, field

    def _get_record_field_from_iter(self, iter_, store=None):
        if not store:
            store = self.view.treeview.get_model()
        record = store.get_value(iter_, 0)
        field = record.group.fields[self.attrs['name']]
        return record, field

    def _set_visual(self, cell, record):
        visual = record.expr_eval(self.attrs.get('visual'))
        if not visual:
            visual = record.expr_eval(self.view.attributes.get('visual'))
        background = COLORS.get(visual) if visual != 'muted' else None
        foreground = COLORS.get(visual) if visual == 'muted' else None
        cell.set_property('cell-background', background)
        if isinstance(cell, Gtk.CellRendererText):
            cell.set_property('foreground', foreground)
            cell.set_property('foreground-set', bool(foreground))

    def set_editable(self):
        pass


class Affix(Cell):
    expand = False

    def __init__(self, view, attrs, protocol=None):
        super(Affix, self).__init__()
        self.attrs = attrs
        self.protocol = protocol
        self.icon = attrs.get('icon')
        if protocol:
            self.renderer = CellRendererClickablePixbuf()
            self.renderer.connect('clicked', self.clicked)
            if not self.icon:
                self.icon = 'tryton-public'
        elif self.icon:
            self.renderer = Gtk.CellRendererPixbuf()
        else:
            self.renderer = Gtk.CellRendererText()
        self.view = view

    @realized
    @CellCache.cache
    def setter(self, column, cell, store, iter_, user_data=None):
        record, field = self._get_record_field_from_iter(iter_, store)
        field = record[self.attrs['name']]
        field.state_set(record, states=('invisible',))
        invisible = field.get_state_attrs(record).get('invisible', False)
        cell.set_property('visible', not invisible)
        if self.icon:
            if self.icon in record.group.fields:
                value = record[self.icon].get_client(record) or ''
            else:
                value = self.icon
            if self.attrs.get('icon_type') == 'url':
                pixbuf = common.IconFactory.get_pixbuf_url(
                    value, size_param=self.attrs.get('url_size'))
            else:
                pixbuf = common.IconFactory.get_pixbuf(
                    value, Gtk.IconSize.BUTTON)
            cell.set_property('pixbuf', pixbuf)
        else:
            text = self.attrs.get('string', '')
            if not text:
                text = field.get_client(record) or ''
            cell.set_property('text', text)
        self._set_visual(cell, record)

    def clicked(self, renderer, path):
        record, field = self._get_record_field_from_path(path)
        value = record[self.attrs['name']].get(record)
        if value:
            if self.protocol == 'email':
                value = 'mailto:%s' % value
            elif self.protocol == 'callto':
                value = 'callto:%s' % value
            elif self.protocol == 'sip':
                value = 'sip:%s' % value
            webbrowser.open(value, new=2)


class Symbol(Cell):
    expand = False

    def __init__(self, view, attrs, position):
        super().__init__()
        self.attrs = attrs
        self.symbol = attrs.get('symbol')
        self.position = position
        self.renderer = Gtk.CellRendererText()
        self.renderer.set_property('yalign', 0)
        self.renderer.set_property('xalign', 0 if position else 1)
        self.view = view

    @realized
    @CellCache.cache
    def setter(self, column, cell, store, iter_, user_data=None):
        record, field = self._get_record_field_from_iter(iter_, store)
        field.state_set(record, states=('invisible',))
        invisible = field.get_state_attrs(record).get('invisible', False)
        if invisible:
            cell.set_property('text', '')
            cell.set_property('visible', not invisible)
            return
        symbol, position = field.get_symbol(record, self.symbol)
        if round(position) == self.position:
            cell.set_property('text', symbol)
            cell.set_property('visible', True)
        else:
            cell.set_property('text', '')
            cell.set_property('visible', False)
        self._set_visual(cell, record)


class GenericText(Cell):
    align = 0
    editable = None
    editing = None

    def __init__(self, view, attrs, renderer=None):
        super(GenericText, self).__init__()
        self.attrs = attrs
        if renderer is None:
            renderer = CellRendererText
        self.renderer = renderer()
        self.renderer.connect('editing-started', self.editing_started)
        self.renderer.connect_after(
            'editing-started', send_keys, view.treeview)
        self.renderer.set_property('yalign', 0)
        self.renderer.set_property('xalign', self.align)
        self.view = view

    @property
    def field_name(self):
        return self.attrs['name']

    @property
    def model_name(self):
        return self.view.screen.model_name

    @realized
    @CellCache.cache
    def setter(self, column, cell, store, iter_, user_data=None):
        record, field = self._get_record_field_from_iter(iter_, store)
        text = self.get_textual_value(record)

        if isinstance(cell, Gtk.CellRendererToggle):
            cell.set_active(bool(text))
        else:
            cell.set_sensitive(not (record.deleted or record.removed))
            if isinstance(cell, Gtk.CellRendererText):
                cell.set_property('strikethrough', record.deleted)
            cell.set_property('text', text)

        states = ('invisible',)
        if self.view.editable:
            states = ('readonly', 'required', 'invisible')

        field.state_set(record, states=states)
        invisible = field.get_state_attrs(record).get('invisible', False)
        cell.set_property('visible', not invisible)
        # Sometimes, the treeview with fixed height mode computes a too big
        # height for not visible cell with text
        # We can force an empty text because not visible cell can not be edited
        # and so value_from_text is never called.
        if invisible and not isinstance(cell, CellRendererToggle):
            cell.set_property('text', '')

        if self.view.editable:
            readonly = self.attrs.get('readonly',
                field.get_state_attrs(record).get('readonly', False))
            if invisible:
                readonly = True

            if isinstance(cell, CellRendererToggle):
                cell.set_property('activatable', not readonly)
            elif isinstance(cell,
                    (Gtk.CellRendererProgress, CellRendererButton,
                        Gtk.CellRendererPixbuf)):
                pass
            else:
                cell.set_property('editable', not readonly)
        else:
            if isinstance(cell, CellRendererToggle):
                cell.set_property('activatable', False)
        self._set_visual(cell, record)

    def open_remote(self, record, create, changed=False, text=None,
            callback=None):
        raise NotImplementedError

    @catch_errors()
    def get_textual_value(self, record):
        record.load(self.attrs['name'], process_exception=False)
        return record[self.attrs['name']].get_client(record)

    def value_from_text(self, record, text, callback=None):
        field = record[self.attrs['name']]
        field.set_client(record, text)
        if callback:
            callback()

    def set_editable(self):
        if not self.editable or not self.editing:
            return
        record, field = self.editing
        self.editable.set_text(self.get_textual_value(record))

    def editing_started(self, cell, editable, path):
        def remove(editable):
            self.editable = None
            self.editing = None
        self.editable = editable
        self.editing = self._get_record_field_from_path(path)
        editable.connect('remove-widget', remove)
        return False

    def get_editable(self, renderer):
        if self.renderer == renderer:
            return self.editable
        for cell in self.prefixes + self.suffixes:
            editable = cell.get_editable(renderer)
            if editable:
                return editable


class Char(GenericText):

    @realized
    @CellCache.cache
    def setter(self, column, cell, store, iter_, user_data=None):
        super(Char, self).setter(column, cell, store, iter_, user_data)
        cell.set_property('single-paragraph-mode', True)


class Text(GenericText):
    pass


class Int(GenericText):
    align = 1

    def __init__(self, view, attrs, renderer=None):
        if renderer is None:
            renderer = CellRendererInteger
        super(Int, self).__init__(view, attrs, renderer=renderer)
        self.factor = float(attrs.get('factor', 1))
        self.symbol = attrs.get('symbol')
        self.grouping = bool(int(attrs.get('grouping', 1)))
        if self.symbol:
            self.renderer_prefix = Symbol(view, attrs, 0)
            self.renderer_suffix = Symbol(view, attrs, 1)

    @property
    def prefixes(self):
        if self.symbol:
            return [self.renderer_prefix]
        return []

    @property
    def suffixes(self):
        if self.symbol:
            return [self.renderer_suffix]
        return []

    @catch_errors()
    def get_textual_value(self, record):
        record.load(self.attrs['name'], process_exception=False)
        return record[self.attrs['name']].get_client(
            record, factor=self.factor, grouping=self.grouping)

    def value_from_text(self, record, text, callback=None):
        field = record[self.attrs['name']]
        field.set_client(record, text, factor=self.factor)
        if callback:
            callback()


class Boolean(GenericText):
    align = 0.5

    def __init__(self, view, attrs=None,
            renderer=None):
        if renderer is None:
            renderer = CellRendererToggle
        super(Boolean, self).__init__(view, attrs, renderer=renderer)
        self.renderer.connect('toggled', self._sig_toggled)

    def _sig_toggled(self, renderer, path):
        record, field = self._get_record_field_from_path(path)
        if (self.view.record and self.view.record != record
                and not self.view.record.validate(self.view.get_fields())):
            renderer.stop_emission_by_name('toggled')
            return True
        if not self.attrs.get('readonly',
                field.get_state_attrs(record).get('readonly', False)):
            value = record[self.attrs['name']].get_client(record)
            record[self.attrs['name']].set_client(record, int(not value))
            self.view.treeview.set_cursor(path)
        return True


class URL(Char):

    @realized
    @CellCache.cache
    def setter(self, column, cell, store, iter_, user_data=None):
        super(URL, self).setter(column, cell, store, iter_, user_data)
        record, field = self._get_record_field_from_iter(iter_, store)
        field.state_set(record, states=('readonly',))
        readonly = field.get_state_attrs(record).get('readonly', False)
        cell.set_property('visible', not readonly)


class Date(GenericText):

    def __init__(self, view, attrs, renderer=None):
        if renderer is None:
            renderer = CellRendererDate
        super(Date, self).__init__(view, attrs, renderer=renderer)

    @realized
    def setter(self, column, cell, store, iter_, user_data=None):
        record, field = self._get_record_field_from_iter(iter_, store)
        self.renderer.props.format = self.get_format(record, field)
        super(Date, self).setter(column, cell, store, iter_, user_data)

    def get_format(self, record, field):
        if field and record:
            return field.date_format(record)
        else:
            return '%x'

    @catch_errors()
    def get_textual_value(self, record):
        record.load(self.attrs['name'], process_exception=False)
        value = record[self.attrs['name']].get_client(record)
        if value:
            return value.strftime(self.renderer.props.format)
        else:
            return ''

    def value_from_text(self, record, text, callback=None):
        if isinstance(text, str):
            field = record[self.attrs['name']]
            try:
                # Use a datetime instance and rely on field to convert to the
                # proper type
                text = date_parse(text, self.get_format(record, field))
            except (ValueError, OverflowError):
                text = None
        return super().value_from_text(record, text, callback=callback)


class Time(Date):

    def __init__(self, view, attrs, renderer=None):
        if renderer is None:
            renderer = CellRendererTime
        super(Time, self).__init__(view, attrs, renderer=renderer)

    def get_format(self, record, field):
        if field and record:
            return field.time_format(record)
        else:
            return '%X'

    @catch_errors()
    def get_textual_value(self, record):
        record.load(self.attrs['name'], process_exception=False)
        value = record[self.attrs['name']].get_client(record)
        if value is not None:
            if isinstance(value, datetime.datetime):
                value = value.time()
            return value.strftime(self.renderer.props.format)
        else:
            return ''

    def set_editable(self):
        if not self.editable or not self.editing:
            return
        record, field = self.editing
        self.editable.get_child().set_text(self.get_textual_value(record))


class TimeDelta(GenericText):
    align = 1


class Float(Int):

    def __init__(self, view, attrs, renderer=None):
        if renderer is None:
            renderer = CellRendererFloat
        super(Float, self).__init__(view, attrs, renderer=renderer)
        self.renderer.monetary = attrs.get('monetary', False)

    @realized
    def setter(self, column, cell, store, iter_, user_data=None):
        super(Float, self).setter(column, cell, store, iter_, user_data)
        record, field = self._get_record_field_from_iter(iter_, store)
        digits = field.digits(record, factor=self.factor)
        cell.digits = digits


class Binary(GenericText):
    align = 1

    def __init__(self, view, attrs, renderer=None):
        if renderer is None:
            renderer = CellRendererText
        super(Binary, self).__init__(view, attrs, renderer=renderer)
        self.renderer.set_property('editable', False)
        self.renderer.set_property('xalign', self.align)
        self.renderer_save = _BinarySave(self)
        self.renderer_select = _BinarySelect(self)
        if self.attrs.get('filename'):
            self.renderer_open = _BinaryOpen(self)
        else:
            self.renderer_open = None

    @property
    def prefixes(self):
        return filter(None, [self.renderer_open])

    @property
    def suffixes(self):
        return [self.renderer_save, self.renderer_select]

    @catch_errors()
    def get_textual_value(self, record):
        record.load(self.attrs['name'], process_exception=False)
        field = record[self.attrs['name']]
        if hasattr(field, 'get_size'):
            size = field.get_size(record)
        else:
            size = len(field.get(record))
        return common.humanize(size, 'B') if size else ''

    def value_from_text(self, record, text, callback=None):
        if callback:
            callback()

    @realized
    @CellCache.cache
    def setter(self, column, cell, store, iter_, user_data=None):
        record, field = self._get_record_field_from_iter(iter_, store)
        text = self.get_textual_value(record)
        cell.set_property('text', text)

        states = ('invisible',)
        if self.view.editable:
            states = ('readonly', 'required', 'invisible')

        field.state_set(record, states=states)
        invisible = field.get_state_attrs(record).get('invisible', False)
        cell.set_property('visible', not invisible)
        self._set_visual(cell, record)

    def get_data(self, record, field):
        if hasattr(field, 'get_data'):
            data = field.get_data(record)
        else:
            data = field.get(record)
        if isinstance(data, str):
            data = data.encode('utf-8')
        return data


class _BinaryIcon(Cell):
    expand = False

    def __init__(self, binary):
        super().__init__()
        self.binary = binary
        self.renderer = CellRendererClickablePixbuf()
        self.renderer.connect('clicked', self.clicked)

    @property
    def attrs(self):
        return self.binary.attrs

    @property
    def view(self):
        return self.binary.view

    @catch_errors(False)
    def _load_data(self, record):
        record.load(self.attrs['name'], process_exception=False)
        return True


class _BinarySave(_BinaryIcon):
    icon_name = 'tryton-download'

    def __init__(self, binary):
        super().__init__(binary)
        pixbuf = common.IconFactory.get_pixbuf(
            self.icon_name, Gtk.IconSize.BUTTON)
        self.renderer.set_property('pixbuf', pixbuf)

    @common.idle_add
    def clicked(self, renderer, path):
        filename = ''
        record, field = self._get_record_field_from_path(path)
        if self.attrs.get('filename'):
            filename_field = record.group.fields.get(self.attrs['filename'])
            filename = filename_field.get(record)
        filename = file_selection(_('Save As...'), filename=filename,
            action=Gtk.FileChooserAction.SAVE)
        if filename:
            with open(filename, 'wb') as fp:
                fp.write(self.binary.get_data(record, field))

    @realized
    @CellCache.cache
    def setter(self, column, cell, store, iter_, user_data=None):
        record, field = self._get_record_field_from_iter(iter_, store)
        if not self._load_data(record):
            cell.set_property('visible', False)
            return
        if hasattr(field, 'get_size'):
            size = field.get_size(record)
        else:
            size = len(field.get(record))
        field.state_set(record, states=['invisible'])
        invisible = field.get_state_attrs(record).get('invisible', False)
        cell.set_property('visible', not invisible and size)
        self._set_visual(cell, record)


class _BinarySelect(_BinaryIcon):
    def clicked(self, renderer, path):
        record, field = self._get_record_field_from_path(path)
        if hasattr(field, 'get_size'):
            size = field.get_size(record)
        else:
            size = len(field.get(record))
        if self.attrs.get('filename'):
            filename_field = record.group.fields[self.attrs['filename']]
        else:
            filename_field = None
        if size:
            if filename_field:
                filename_field.set_client(record, None)
            field.set_client(record, None)
        else:
            def _select():
                filename = file_selection(_('Open...'))
                if filename:
                    with open(filename, 'rb') as fp:
                        field.set_client(record, fp.read())
                    if filename_field:
                        filename_field.set_client(
                            record, os.path.basename(filename))
            GLib.idle_add(_select)

    @realized
    @CellCache.cache
    def setter(self, column, cell, store, iter_, user_data=None):
        record, field = self._get_record_field_from_iter(iter_, store)
        if not self._load_data(record):
            cell.set_property('visible', False)
            return
        if hasattr(field, 'get_size'):
            size = field.get_size(record)
        else:
            size = len(field.get(record))
        if size:
            icon = 'tryton-clear'
        else:
            icon = 'tryton-search'
        pixbuf = common.IconFactory.get_pixbuf(icon, Gtk.IconSize.BUTTON)
        cell.set_property('pixbuf', pixbuf)
        field.state_set(record, states=['invisible', 'readonly'])
        invisible = field.get_state_attrs(record).get('invisible', False)
        readonly = self.attrs.get('readonly',
            field.get_state_attrs(record).get('readonly', False))
        if readonly or size:
            cell.set_property('visible', False)
        else:
            cell.set_property('visible', not invisible)
        self._set_visual(cell, record)


class _BinaryOpen(_BinarySave):
    icon_name = 'tryton-open'

    def clicked(self, renderer, path):
        record, field = self._get_record_field_from_path(path)
        filename_field = record.group.fields.get(self.attrs.get('filename'))
        filename = filename_field.get(record)
        file_path = file_write(filename, self.binary.get_data(record, field))
        root, type_ = os.path.splitext(filename)
        if type_:
            type_ = type_[1:]
        GLib.idle_add(file_open, file_path, type_)

    @realized
    @CellCache.cache
    def setter(self, column, cell, store, iter_, user_data=None):
        super().setter(column, cell, store, iter_)
        record, field = self._get_record_field_from_iter(iter_, store)
        if not self._load_data(record):
            cell.set_property('visible', False)
            return
        filename_field = record.group.fields.get(self.attrs.get('filename'))
        filename = filename_field.get(record)
        if not filename:
            cell.set_property('visible', False)


class Image(GenericText):
    align = 0.5

    def __init__(self, view, attrs=None, renderer=None):
        if renderer is None:
            renderer = Gtk.CellRendererPixbuf
        super(Image, self).__init__(view, attrs, renderer)
        self.height = int(attrs.get('height', 100))
        self.width = int(attrs.get('width', 300))
        self.renderer.set_fixed_size(self.width, self.height)

    @realized
    @CellCache.cache
    def setter(self, column, cell, store, iter_, user_data=None):
        record, field = self._get_record_field_from_iter(iter_, store)
        value = self._get_data(record)
        if value is None:
            cell.set_property('pixbuf', None)
            return
        pixbuf = data2pixbuf(value)
        if pixbuf:
            pixbuf = common.resize_pixbuf(pixbuf, self.width, self.height)
        cell.set_property('pixbuf', pixbuf)
        self._set_visual(cell, record)

    @catch_errors(None)
    def _get_data(self, record):
        record.load(self.attrs['name'], process_exception=False)
        field = record[self.attrs['name']]
        value = field.get_client(record)
        if isinstance(value, int):
            if value > CONFIG['image.max_size']:
                value = None
            else:
                value = field.get_data(record)
        return value

    @catch_errors()
    def get_textual_value(self, record):
        record.load(self.attrs['name'], process_exception=False)
        field = record[self.attrs['name']]
        return str(field.get_size(record))


class M2O(GenericText):

    def __init__(self, view, attrs, renderer=None):
        if renderer is None and int(attrs.get('completion', 1)):
            renderer = partial(CellRendererTextCompletion, self.set_completion)
        super(M2O, self).__init__(view, attrs, renderer=renderer)

    def get_model(self, record, field):
        return self.attrs['relation']

    def has_target(self, value):
        return value is not None

    def value_from_id(self, model, id_, str_=None):
        if str_ is None:
            str_ = ''
        return id_, str_

    def id_from_value(self, value):
        return value

    def value_from_text(self, record, text, callback=None):
        field = record.group.fields[self.attrs['name']]
        model = self.get_model(record, field)
        if not text:
            field.set_client(
                record, self.value_from_id(model, None, ''))
            if callback:
                callback()
            return

        if model and common.get_toplevel_window().get_focus():
            field = record[self.attrs['name']]
            win = self.search_remote(record, field, text, callback=callback)
            win.show()

    def editing_started(self, cell, editable, path):
        super(M2O, self).editing_started(cell, editable, path)
        record, field = self._get_record_field_from_path(path)
        model = self.get_model(record, field)

        def changed(editable):
            text = editable.get_text()
            if self.get_textual_value(record) != text:
                field.set_client(
                    record, self.value_from_id(model, None, ''))

            if self.has_target(field.get(record)):
                icon1, tooltip1 = 'tryton-open', _("Open the record <F2>")
                icon2, tooltip2 = 'tryton-clear', _("Clear the field <Del>")
            else:
                icon1, tooltip1 = None, ''
                icon2, tooltip2 = 'tryton-search', _("Search a record <F2>")
            for pos, icon, tooltip in [
                    (Gtk.EntryIconPosition.PRIMARY, icon1, tooltip1),
                    (Gtk.EntryIconPosition.SECONDARY, icon2, tooltip2)]:
                if icon:
                    pixbuf = common.IconFactory.get_pixbuf(
                        icon, Gtk.IconSize.MENU)
                else:
                    pixbuf = None
                editable.set_icon_from_pixbuf(pos, pixbuf)
                editable.set_icon_tooltip_text(pos, tooltip)

        def icon_press(editable, icon_pos, event):
            editable.grab_focus()
            value = field.get(record)
            if not model:
                return
            if (icon_pos == Gtk.EntryIconPosition.SECONDARY
                    and self.has_target(value)):
                field.set_client(
                    record, self.value_from_id(model, None, ''))
                editable.set_text('')
            elif self.has_target(value):
                self.open_remote(record, create=False, changed=False)
            else:
                self.open_remote(
                    record, create=False, changed=True,
                    text=editable.get_text())

        editable.connect('icon-press', icon_press)
        editable.connect('changed', changed)
        changed(editable)
        return False

    def open_remote(
            self, record, create=True, changed=False, text=None, defaults=None,
            callback=None):
        field = record.group.fields[self.attrs['name']]
        model = self.get_model(record, field)

        access = common.MODELACCESS[model]
        if (create
                and not (int(self.attrs.get('create', 1))
                    and access['create'])):
            return
        elif not access['read']:
            return

        domain = field.domain_get(record)
        context = field.get_context(record)
        if not create and changed:
            self.search_remote(record, field, text, callback=callback).show()
            return
        target_id = self.id_from_value(field.get(record))

        # Remove first tree view as mode is form only
        view_ids = self.attrs.get('view_ids', '').split(',')[1:]
        breadcrumb = list(self.view.screen.breadcrumb)
        breadcrumb.append(
            field.attrs.get('string') or common.MODELNAME.get(model))
        screen = Screen(model, domain=domain, context=context,
            mode=['form'], view_ids=view_ids,
            exclude_field=field.attrs.get('relation_field'),
            breadcrumb=breadcrumb)

        def open_callback(result):
            if result:
                value = self.value_from_id(
                    model,
                    screen.current_record.id,
                    screen.current_record.rec_name())
                field.set_client(record, value, force_change=True)
            if callback:
                callback()
        if target_id and target_id >= 0:
            screen.load([target_id])
            screen.current_record = screen.group.get(target_id)
            WinForm(screen, open_callback, save_current=True)
        else:
            defaults = defaults.copy() if defaults is not None else {}
            defaults['rec_name'] = text
            WinForm(
                screen, open_callback, new=True, save_current=True,
                defaults=defaults)

    def search_remote(self, record, field, text, callback=None):
        model = self.get_model(record, field)
        domain = field.domain_get(record)
        context = field.get_search_context(record)
        order = field.get_search_order(record)
        access = common.MODELACCESS[model]
        create_access = int(self.attrs.get('create', 1)) and access['create']

        def search_callback(found):
            value = None
            if found:
                value = self.value_from_id(model, *found[0])
                field.set_client(record, value)
            if callback:
                callback()
        win = WinSearch(model, search_callback, sel_multi=False,
            context=context, domain=domain,
            order=order, view_ids=self.attrs.get('view_ids', '').split(','),
            new=create_access, title=self.attrs.get('string'))
        win.screen.search_filter(quote(text))
        return win

    def set_completion(self, entry, path):
        record, field = self._get_record_field_from_path(path)
        if entry.get_completion():
            entry.set_completion(None)
        model = self.get_model(record, field)
        if not model:
            return
        access = common.MODELACCESS[model]
        completion = get_completion(
            search=access['read'],
            create=int(self.attrs.get('create', 1)) and access['create'])
        completion.connect('match-selected', self._completion_match_selected,
            record, field, model)
        completion.connect('action-activated',
            self._completion_action_activated, record, field)
        entry.set_completion(completion)
        entry.connect('key-press-event', self._key_press, record, field)
        entry.connect('changed', self._update_completion, record, field)

    def _key_press(self, entry, event, record, field):
        if (self.has_target(field.get(record))
                and event.keyval in [Gdk.KEY_Delete, Gdk.KEY_BackSpace]):
            entry.set_text('')
        return False

    def _completion_match_selected(
            self, completion, model, iter_, record, field, model_name):
        rec_name, record_id, defaults = model.get(iter_, 0, 1, 2)
        if record_id is not None:
            field.set_client(
                record, self.value_from_id(model_name, record_id, rec_name))

            completion.get_entry().set_text(rec_name)
            completion_model = completion.get_model()
            completion_model.clear()
            completion_model.search_text = rec_name
        else:
            entry = completion.get_entry()
            entry.handler_block(entry.editing_done_id)

            def callback():
                entry.handler_unblock(entry.editing_done_id)
                entry.set_text(field.get_client(record))
            self.open_remote(
                record, create=True, text=entry.get_text(), defaults=defaults,
                callback=callback)
        return True

    def _update_completion(self, entry, record, field):
        value = field.get(record)
        if self.has_target(value):
            id_ = self.id_from_value(value)
            if id_ is not None and id_ >= 0:
                return
        model = self.get_model(record, field)
        update_completion(entry, record, field, model)

    def _completion_action_activated(self, completion, index, record, field):
        entry = completion.get_entry()
        entry.handler_block(entry.editing_done_id)

        def callback():
            entry.handler_unblock(entry.editing_done_id)
            entry.set_text(field.get_client(record))
        if index == 0:
            self.open_remote(record, create=False, changed=True,
                text=entry.get_text(), callback=callback)
        elif index == 1:
            self.open_remote(record, create=True,
                text=entry.get_text(), callback=callback)
        else:
            entry.handler_unblock(entry.editing_done_id)


class O2O(M2O):
    pass


class O2M(GenericText):
    align = 0.5

    @catch_errors()
    def get_textual_value(self, record):
        record.load(self.attrs['name'], process_exception=False)
        field = record[self.attrs['name']]
        return '( ' + str(len(field.get_eval(record))) + ' )'

    def value_from_text(self, record, text, callback=None):
        if callback:
            callback()

    def open_remote(self, record, create=True, changed=False, text=None,
            callback=None):
        group = record.value[self.attrs['name']]
        field = record.group.fields[self.attrs['name']]
        relation = field.attrs['relation']
        context = field.get_context(record)

        access = common.MODELACCESS[relation]
        if not access['read']:
            return

        breadcrumb = list(self.view.screen.breadcrumb)
        breadcrumb.append(
            field.attrs.get('string') or common.MODELNAME.get(relation))
        screen = Screen(relation, mode=['tree', 'form'],
            view_ids=self.attrs.get('view_ids', '').split(','),
            exclude_field=field.attrs.get('relation_field'),
            limit=None,
            context=self.view.screen.context,
            breadcrumb=breadcrumb)
        screen.pre_validate = bool(int(self.attrs.get('pre_validate', 0)))
        screen.group = group

        def open_callback(result):
            if callback:
                callback()
        WinForm(screen, open_callback, view_type='tree', context=context)


class M2M(O2M):

    def open_remote(self, record, create=True, changed=False, text=None,
            callback=None):
        group = record.value[self.attrs['name']]
        field = record.group.fields[self.attrs['name']]
        relation = field.attrs['relation']
        context = field.get_context(record)
        domain = field.domain_get(record)

        breadcrumb = list(self.view.screen.breadcrumb)
        breadcrumb.append(
            field.attrs.get('string') or common.MODELNAME.get(relation))
        screen = Screen(relation, mode=['tree', 'form'],
            view_ids=self.attrs.get('view_ids', '').split(','),
            exclude_field=field.attrs.get('relation_field'),
            limit=None,
            context=self.view.screen.context,
            breadcrumb=breadcrumb)
        screen.group = group

        def open_callback(result):
            if callback:
                callback()
        WinForm(screen, open_callback, view_type='tree', domain=domain,
            context=context)


class Selection(GenericText, SelectionMixin, PopdownMixin):

    def __init__(self, *args, **kwargs):
        if 'renderer' not in kwargs:
            kwargs['renderer'] = CellRendererCombo
        super(Selection, self).__init__(*args, **kwargs)
        if self.view and self.view.editable:
            self.init_selection()
            # Use a variable let Python holding reference when calling
            # set_property
            model = self.get_popdown_model(self.selection)[0]
            self.renderer.set_property('model', model)
            self.renderer.set_property('text-column', 0)

    def get_value(self, record, field):
        return field.get(record)

    @catch_errors()
    def get_textual_value(self, record):
        related = self.attrs['name'] + ':string'
        if not self.view.editable and related in record.value:
            return record.value[related]

        record.load(self.attrs['name'], process_exception=False)
        field = record[self.attrs['name']]
        self.update_selection(record, field)
        value = self.get_value(record, field)
        text = dict(self.selection).get(value, '')
        if value and not text:
            text = self.get_inactive_selection(value)
        return text

    def value_from_text(self, record, text, callback=None):
        if callback:
            callback()

    def set_editable(self):
        if not self.editable and not self.editing:
            return
        record, field = self.editing
        value = self.get_value(record, field)
        self.update_selection(record, field)
        self.set_popdown_value(self.editable, value)

    def editing_started(self, cell, editable, path):
        super(Selection, self).editing_started(cell, editable, path)
        record, field = self._get_record_field_from_path(path)
        gdk_backend = get_gdk_backend()
        if gdk_backend == 'x11':
            # Combobox does not emit remove-widget when focus is changed
            self.editable.connect(
                'editing-done',
                lambda *a: self.editable.emit('remove-widget'))

        selection_shortcuts(editable)

        def set_value(*a):
            return self.set_value(editable, record, field)
        editable.get_child().connect('activate', set_value)
        editable.get_child().connect('focus-out-event', set_value)

        if gdk_backend != 'x11':
            # For non X11 backend we can rely the focus-out-event to be sent
            # correctly
            def remove_entry(entry, event):
                editable.emit('editing-done')
                editable.emit('remove-widget')
            editable.get_child().connect('focus-out-event', remove_entry)
        editable.connect('changed', set_value)

        self.update_selection(record, field)
        self.set_popdown(self.selection, editable)

        value = self.get_value(record, field)
        if not self.set_popdown_value(editable, value):
            self.get_inactive_selection(value)
            self.set_popdown_value(editable, value)
        return False

    def set_value(self, editable, record, field):
        value = self.get_popdown_value(editable)
        if 'relation' in self.attrs and value:
            active = editable.get_active()
            if active < 0:
                text = None
            else:
                model = editable.get_model()
                index = editable.get_property('entry-text-column')
                text = model[active][index]
            value = (value, text)
        field.set_client(record, value)
        return False


class MultiSelection(GenericText, SelectionMixin):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.view and self.view.editable:
            self.init_selection()

    @realized
    @CellCache.cache
    def setter(self, column, cell, store, iter_, user_data=None):
        super().setter(column, cell, store, iter_, user_data=user_data)
        cell.set_property('editable', False)

    def value_from_text(self, record, text, callback=None):
        if callback:
            callback()

    @catch_errors()
    def get_textual_value(self, record):
        related = self.attrs['name'] + ':string'
        if not self.view.editable and related in record.value:
            return ";".join(record.value[related])

        record.load(self.attrs['name'], process_exception=False)
        field = record[self.attrs['name']]
        self.update_selection(record, field)
        selection = dict(self.selection)
        values = []
        for value in field.get_eval(record):
            text = selection.get(value, '')
            values.append(text)
        return ';'.join(values)


class Reference(M2O):

    def __init__(self, view, attrs, renderer=None):
        super(Reference, self).__init__(view, attrs, renderer=renderer)
        self.renderer_selection = _ReferenceSelection(view, attrs)

    @property
    def prefixes(self):
        return [self.renderer_selection]

    def get_model(self, record, field):
        value = field.get_client(record)
        if value:
            model, value = value
            return model

    def has_target(self, value):
        if not value:
            return False
        model, value = value.split(',')
        if not value:
            value = None
        else:
            try:
                value = int(value)
            except ValueError:
                value = None
        return model and value >= 0

    def value_from_id(self, model, id_, str_=None):
        if str_ is None:
            str_ = ''
        return model, (id_, str_)

    def id_from_value(self, value):
        _, value = value.split(',')
        return int(value)

    def get_textual_value(self, record):
        value = super().get_textual_value(record)
        if isinstance(value, tuple):
            model, value = value
        return value


class _ReferenceSelection(Selection):

    def get_value(self, record, field):
        value = field.get_client(record)
        if value:
            model, value = value
        else:
            model = None
        return model

    def set_value(self, editable, record, field):
        value = self.get_popdown_value(editable)
        if value != self.get_value(record, field):
            if value:
                value = (value, (-1, ''))
            else:
                value = ('', '')
            field.set_client(record, value)
        return False


class Dict(GenericText):
    align = 0.5

    def __init__(self, view, attrs):
        super().__init__(view, attrs)
        self.renderer.props.editable = False

    def setter(self, column, cell, store, iter_, user_data=None):
        super().setter(column, cell, store, iter_, user_data=None)
        cell.props.editable = False

    @catch_errors()
    def get_textual_value(self, record):
        record.load(self.attrs['name'], process_exception=False)
        field = record[self.attrs['name']]
        return '(%s)' % len(field.get_client(record))


class ProgressBar(Cell):
    align = 0.5
    orientations = {
        'left_to_right': (Gtk.Orientation.HORIZONTAL, False),
        'right_to_left': (Gtk.Orientation.HORIZONTAL, True),
        'bottom_to_top': (Gtk.Orientation.VERTICAL, True),
        'top_to_bottom': (Gtk.Orientation.VERTICAL, False),
        }

    def __init__(self, view, attrs):
        super(ProgressBar, self).__init__()
        self.view = view
        self.attrs = attrs
        self.renderer = Gtk.CellRendererProgress()
        orientation, inverted = self.orientations.get(
            self.attrs.get('orientation', 'left_to_right'))
        self.renderer.set_orientation(orientation)
        self.renderer.set_property('inverted', inverted)
        self.renderer.set_property('yalign', 0)

    @realized
    @CellCache.cache
    def setter(self, column, cell, store, iter_, user_data=None):
        record, field = self._get_record_field_from_iter(iter_, store)
        field.state_set(record, states=('invisible',))
        invisible = field.get_state_attrs(record).get('invisible', False)
        cell.set_property('visible', not invisible)
        text = self.get_textual_value(record)
        if text:
            text = _('%s%%') % text
        cell.set_property('text', text)
        value = field.get(record) or 0.0
        cell.set_property('value', value * 100)
        self._set_visual(cell, record)

    def open_remote(self, record, create, changed=False, text=None,
            callback=None):
        raise NotImplementedError

    @catch_errors()
    def get_textual_value(self, record):
        record.load(self.attrs['name'], process_exception=False)
        field = record[self.attrs['name']]
        return field.get_client(record, factor=100) or ''

    def value_from_text(self, record, text, callback=None):
        field = record[self.attrs['name']]
        field.set_client(record, float(text))
        if callback:
            callback()


class Button(Cell):

    def __init__(self, view, attrs):
        super(Button, self).__init__()
        self.attrs = attrs
        self.renderer = CellRendererButton(attrs.get('string', _('Unknown')))
        self.view = view

        self.renderer.connect('clicked', self.button_clicked)
        self.renderer.set_property('yalign', 0)

    @realized
    @CellCache.cache
    def setter(self, column, cell, store, iter_, user_data=None):
        record = store.get_value(iter_, 0)
        states = record.expr_eval(self.attrs.get('states', {}))
        invisible = states.get('invisible', False)
        cell.set_property('visible', not invisible)
        readonly = states.get('readonly', False)
        cell.set_property('sensitive', not readonly)
        if self.attrs.get('type', 'class') == 'class':
            parent = record.parent if record else None
            while parent:
                if parent.modified:
                    cell.set_property('sensitive', False)
                    break
                parent = parent.parent
        # TODO icon
        self._set_visual(cell, record)

    @common.idle_add
    def button_clicked(self, widget, path):
        if not path:
            return True
        store = self.view.treeview.get_model()
        record = store.get_value(store.get_iter(path), 0)

        if self.view.record and self.view.record != record:
            widget.stop_emission_by_name('clicked')
            return True

        state_changes = record.expr_eval(
            self.attrs.get('states', {}))
        if state_changes.get('invisible') \
                or state_changes.get('readonly'):
            return True

        self.view.treeview.set_cursor(path)
        widget.handler_block_by_func(self.button_clicked)
        try:
            self.view.screen.button(self.attrs)
        finally:
            widget.handler_unblock_by_func(self.button_clicked)
