# fmt: off

# type: ignore
import platform
import re
import tkinter as tk
import tkinter.ttk as ttk
from collections import namedtuple
from functools import partial
from tkinter.filedialog import LoadFileDialog, SaveFileDialog
from tkinter.messagebox import askokcancel as ask_question
from tkinter.messagebox import showerror, showinfo, showwarning

import numpy as np

from ase.gui.i18n import _

__all__ = [
    'error', 'ask_question', 'MainWindow', 'LoadFileDialog', 'SaveFileDialog',
    'ASEGUIWindow', 'Button', 'CheckButton', 'ComboBox', 'Entry', 'Label',
    'Window', 'MenuItem', 'RadioButton', 'RadioButtons', 'Rows', 'Scale',
    'showinfo', 'showwarning', 'SpinBox', 'Text']


def error(title, message=None):
    if message is None:
        message = title
        title = _('Error')
    return showerror(title, message)


def about(name, version, webpage):
    text = [name,
            '',
            _('Version') + ': ' + version,
            _('Web-page') + ': ' + webpage]
    win = Window(_('About'))
    win.add(Text('\n'.join(text)))


def helpbutton(text):
    return Button(_('Help'), helpwindow, text)


def helpwindow(text):
    win = Window(_('Help'))
    win.add(Text(text))


class BaseWindow:
    def __init__(self, title, close=None):
        self.title = title
        if close:
            self.win.protocol('WM_DELETE_WINDOW', close)
        else:
            self.win.protocol('WM_DELETE_WINDOW', self.close)

        self.things = []
        self.exists = True

    def close(self):
        self.win.destroy()
        self.exists = False

    def title(self, txt):
        self.win.title(txt)

    title = property(None, title)

    def add(self, stuff, anchor='w'):  # 'center'):
        if isinstance(stuff, str):
            stuff = Label(stuff)
        elif isinstance(stuff, list):
            stuff = Row(stuff)
        stuff.pack(self.win, anchor=anchor)
        self.things.append(stuff)


class Window(BaseWindow):
    def __init__(self, title, close=None):
        self.win = tk.Toplevel()
        super().__init__(title, close)


class Widget:
    def pack(self, parent, side='top', anchor='center'):
        widget = self.create(parent)
        widget.pack(side=side, anchor=anchor)
        if not isinstance(self, (Rows, RadioButtons)):
            pass

    def grid(self, parent):
        widget = self.create(parent)
        widget.grid()

    def create(self, parent):
        self.widget = self.creator(parent)
        return self.widget

    @property
    def active(self):
        return self.widget['state'] == 'normal'

    @active.setter
    def active(self, value):
        self.widget['state'] = ['disabled', 'normal'][bool(value)]


class Row(Widget):
    def __init__(self, things):
        self.things = things

    def create(self, parent):
        self.widget = tk.Frame(parent)
        for thing in self.things:
            if isinstance(thing, str):
                thing = Label(thing)
            thing.pack(self.widget, 'left')
        return self.widget

    def __getitem__(self, i):
        return self.things[i]


class Label(Widget):
    def __init__(self, text='', color=None):
        self.creator = partial(tk.Label, text=text, fg=color)

    @property
    def text(self):
        return self.widget['text']

    @text.setter
    def text(self, new):
        self.widget.config(text=new)


class Text(Widget):
    def __init__(self, text):
        self.creator = partial(tk.Text, height=text.count('\n') + 1)
        s = re.split('<(.*?)>', text)
        self.text = [(s[0], ())]
        i = 1
        tags = []
        while i < len(s):
            tag = s[i]
            if tag[0] != '/':
                tags.append(tag)
            else:
                tags.pop()
            self.text.append((s[i + 1], tuple(tags)))
            i += 2

    def create(self, parent):
        widget = Widget.create(self, parent)
        widget.tag_configure('sub', offset=-6)
        widget.tag_configure('sup', offset=6)
        widget.tag_configure('c', foreground='blue')
        for text, tags in self.text:
            widget.insert('insert', text, tags)
        widget.configure(state='disabled', background=parent['bg'])
        widget.bind("<1>", lambda event: widget.focus_set())
        return widget


class Button(Widget):
    def __init__(self, text, callback, *args, **kwargs):
        self.callback = partial(callback, *args, **kwargs)
        self.creator = partial(tk.Button,
                               text=text,
                               command=self.callback)


class CheckButton(Widget):
    def __init__(self, text, value=False, callback=None):
        self.text = text
        self.var = tk.BooleanVar(value=value)
        self.callback = callback

    def create(self, parent):
        self.check = tk.Checkbutton(parent, text=self.text,
                                    var=self.var, command=self.callback)
        return self.check

    @property
    def value(self):
        return self.var.get()


class SpinBox(Widget):
    def __init__(self, value, start, end, step, callback=None,
                 rounding=None, width=6):
        self.callback = callback
        self.rounding = rounding
        self.creator = partial(tk.Spinbox,
                               from_=start,
                               to=end,
                               increment=step,
                               command=callback,
                               width=width)
        self.initial = str(value)

    def create(self, parent):
        self.widget = self.creator(parent)
        bind_enter(self.widget, lambda event: self.callback())
        self.value = self.initial
        return self.widget

    @property
    def value(self):
        x = self.widget.get().replace(',', '.')
        if '.' in x:
            return float(x)
        if x == 'None':
            return None
        return int(x)

    @value.setter
    def value(self, x):
        self.widget.delete(0, 'end')
        if '.' in str(x) and self.rounding is not None:
            try:
                x = round(float(x), self.rounding)
            except (ValueError, TypeError):
                pass
        self.widget.insert(0, x)


# Entry and ComboBox use same mechanism (since ttk ComboBox
# is a subclass of tk Entry).
def _set_entry_value(widget, value):
    widget.delete(0, 'end')
    widget.insert(0, value)


class Entry(Widget):
    def __init__(self, value='', width=20, callback=None):
        self.creator = partial(tk.Entry,
                               width=width)
        if callback is not None:
            self.callback = lambda event: callback()
        else:
            self.callback = None
        self.initial = value

    def create(self, parent):
        self.entry = self.creator(parent)
        self.value = self.initial
        if self.callback:
            bind_enter(self.entry, self.callback)
        return self.entry

    @property
    def value(self):
        return self.entry.get()

    @value.setter
    def value(self, x):
        _set_entry_value(self.entry, x)


class Scale(Widget):
    def __init__(self, value, start, end, callback):
        def command(val):
            callback(int(val))

        self.creator = partial(tk.Scale,
                               from_=start,
                               to=end,
                               orient='horizontal',
                               command=command)
        self.initial = value

    def create(self, parent):
        self.scale = self.creator(parent)
        self.value = self.initial
        return self.scale

    @property
    def value(self):
        return self.scale.get()

    @value.setter
    def value(self, x):
        self.scale.set(x)


class RadioButtons(Widget):
    def __init__(self, labels, values=None, callback=None, vertical=False):
        self.var = tk.IntVar()

        if callback:
            def callback2():
                callback(self.value)
        else:
            callback2 = None

        self.values = values or list(range(len(labels)))
        self.buttons = [RadioButton(label, i, self.var, callback2)
                        for i, label in enumerate(labels)]
        self.vertical = vertical

    def create(self, parent):
        self.widget = frame = tk.Frame(parent)
        side = 'top' if self.vertical else 'left'
        for button in self.buttons:
            button.create(frame).pack(side=side)
        return frame

    @property
    def value(self):
        return self.values[self.var.get()]

    @value.setter
    def value(self, value):
        self.var.set(self.values.index(value))

    def __getitem__(self, value):
        return self.buttons[self.values.index(value)]


class RadioButton(Widget):
    def __init__(self, label, i, var, callback):
        self.creator = partial(tk.Radiobutton,
                               text=label,
                               var=var,
                               value=i,
                               command=callback)


if ttk is not None:
    class ComboBox(Widget):
        def __init__(self, labels, values=None, callback=None):
            self.values = values or list(range(len(labels)))
            self.callback = callback
            self.creator = partial(ttk.Combobox,
                                   values=labels)

        def create(self, parent):
            widget = Widget.create(self, parent)
            widget.current(0)
            if self.callback:
                def callback(event):
                    self.callback(self.value)
                widget.bind('<<ComboboxSelected>>', callback)

            return widget

        @property
        def value(self):
            return self.values[self.widget.current()]

        @value.setter
        def value(self, val):
            _set_entry_value(self.widget, val)
else:
    # Use Entry object when there is no ttk:
    def ComboBox(labels, values, callback):
        return Entry(values[0], callback=callback)


class Rows(Widget):
    def __init__(self, rows=None):
        self.rows_to_be_added = rows or []
        self.creator = tk.Frame
        self.rows = []

    def create(self, parent):
        widget = Widget.create(self, parent)
        for row in self.rows_to_be_added:
            self.add(row)
        self.rows_to_be_added = []
        return widget

    def add(self, row):
        if isinstance(row, str):
            row = Label(row)
        elif isinstance(row, list):
            row = Row(row)
        row.grid(self.widget)
        self.rows.append(row)

    def clear(self):
        while self.rows:
            del self[0]

    def __getitem__(self, i):
        return self.rows[i]

    def __delitem__(self, i):
        widget = self.rows.pop(i).widget
        widget.grid_remove()
        widget.destroy()

    def __len__(self):
        return len(self.rows)


class MenuItem:
    def __init__(self, label, callback=None, key=None,
                 value=None, choices=None, submenu=None, disabled=False):
        self.underline = label.find('_')
        self.label = label.replace('_', '')

        is_macos = platform.system() == 'Darwin'

        if key:
            parts = key.split('+')
            modifiers = []
            key_char = None

            for part in parts:
                if part in ('Alt', 'Shift'):
                    modifiers.append(part)
                elif part == 'Ctrl':
                    modifiers.append('Control')
                elif len(part) == 1 and 'Shift' in modifiers:
                    # If shift and letter, uppercase
                    key_char = part
                else:
                    # Lower case
                    key_char = part.lower()

            if is_macos:
                modifiers = ['Command' if m == 'Alt' else m for m in modifiers]

            if modifiers and key_char:
                self.keyname = f"<{'-'.join(modifiers)}-{key_char}>"
            else:
                # Handle special non-modifier keys
                self.keyname = {
                    'Home': '<Home>',
                    'End': '<End>',
                    'Page-Up': '<Prior>',
                    'Page-Down': '<Next>',
                    'Backspace': '<BackSpace>'
                }.get(key, key.lower())
        else:
            self.keyname = None

        if key:
            def callback2(event=None):
                callback(key)

            callback2.__name__ = callback.__name__
            self.callback = callback2
        else:
            self.callback = callback

        if is_macos and key is not None:
            self.key = key.replace('Alt', 'Command')
        else:
            self.key = key
        self.value = value
        self.choices = choices
        self.submenu = submenu
        self.disabled = disabled

    def addto(self, menu, window, stuff=None):
        callback = self.callback
        if self.label == '---':
            menu.add_separator()
        elif self.value is not None:
            var = tk.BooleanVar(value=self.value)
            stuff[self.callback.__name__.replace('_', '-')] = var

            menu.add_checkbutton(label=self.label,
                                 underline=self.underline,
                                 command=self.callback,
                                 accelerator=self.key,
                                 var=var)

            def callback(key):  # noqa: F811
                var.set(not var.get())
                self.callback()

        elif self.choices:
            submenu = tk.Menu(menu)
            menu.add_cascade(label=self.label, menu=submenu)
            var = tk.IntVar()
            var.set(0)
            stuff[self.callback.__name__.replace('_', '-')] = var
            for i, choice in enumerate(self.choices):
                submenu.add_radiobutton(label=choice.replace('_', ''),
                                        underline=choice.find('_'),
                                        command=self.callback,
                                        value=i,
                                        var=var)
        elif self.submenu:
            submenu = tk.Menu(menu)
            menu.add_cascade(label=self.label,
                             menu=submenu)
            for thing in self.submenu:
                thing.addto(submenu, window)
        else:
            state = 'normal'
            if self.disabled:
                state = 'disabled'
            menu.add_command(label=self.label,
                             underline=self.underline,
                             command=self.callback,
                             accelerator=self.key,
                             state=state)
        if self.key:
            window.bind(self.keyname, callback)


class MainWindow(BaseWindow):
    def __init__(self, title, close=None, menu=[]):
        self.win = tk.Tk()
        super().__init__(title, close)

        # self.win.tk.call('tk', 'scaling', 3.0)
        # self.win.tk.call('tk', 'scaling', '-displayof', '.', 7)

        self.menu = {}

        if menu:
            self.create_menu(menu)

    def create_menu(self, menu_description):
        menu = tk.Menu(self.win)
        self.win.config(menu=menu)

        for label, things in menu_description:
            submenu = tk.Menu(menu)
            menu.add_cascade(label=label.replace('_', ''),
                             underline=label.find('_'),
                             menu=submenu)
            for thing in things:
                thing.addto(submenu, self.win, self.menu)

    def resize_event(self):
        # self.scale *= sqrt(1.0 * self.width * self.height / (w * h))
        self.draw()
        self.configured = True

    def run(self):
        # Workaround for nasty issue with tkinter on Mac:
        # https://gitlab.com/ase/ase/issues/412
        #
        # It is apparently a compatibility issue between Python and Tkinter.
        # Some day we should remove this hack.
        while True:
            try:
                tk.mainloop()
                break
            except UnicodeDecodeError:
                pass

    def __getitem__(self, name):
        return self.menu[name].get()

    def __setitem__(self, name, value):
        return self.menu[name].set(value)


def bind(callback, modifier=None):
    def handle(event):
        event.button = event.num
        event.key = event.keysym.lower()
        event.modifier = modifier
        callback(event)
    return handle


class ASEFileChooser(LoadFileDialog):
    def __init__(self, win, formatcallback=lambda event: None):
        from ase.io.formats import all_formats, get_ioformat
        super().__init__(win, _('Open ...'))
        labels = [_('Automatic')]
        values = ['']

        def key(item):
            return item[1][0]

        for format, (description, code) in sorted(all_formats.items(),
                                                  key=key):
            io = get_ioformat(format)
            if io.can_read and description != '?':
                labels.append(_(description))
                values.append(format)

        self.format = None

        def callback(value):
            self.format = value

        Label(_('Choose parser:')).pack(self.top)
        formats = ComboBox(labels, values, callback)
        formats.pack(self.top)


def show_io_error(filename, err):
    showerror(_('Read error'),
              _(f'Could not read {filename}: {err}'))


class ASEGUIWindow(MainWindow):
    def __init__(self, close, menu, config,
                 scroll, scroll_event,
                 press, move, release, resize):
        super().__init__('ASE-GUI', close, menu)

        self.size = np.array([450, 450])

        self.fg = config['gui_foreground_color']
        self.bg = config['gui_background_color']

        self.canvas = tk.Canvas(self.win,
                                width=self.size[0],
                                height=self.size[1],
                                bg=self.bg,
                                highlightthickness=0)
        self.canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        self.status = tk.Label(self.win, text='', anchor=tk.W)
        self.status.pack(side=tk.BOTTOM, fill=tk.X)

        self.canvas.bind('<ButtonPress>', bind(press))
        for button in range(1, 4):
            self.canvas.bind(f'<B{button}-Motion>', bind(move))
        self.canvas.bind('<ButtonRelease>', bind(release))
        self.canvas.bind('<Control-ButtonRelease>', bind(release, 'ctrl'))
        self.canvas.bind('<Shift-ButtonRelease>', bind(release, 'shift'))
        self.canvas.bind('<Configure>', resize)
        if not config['swap_mouse']:
            for button in (2, 3):
                self.canvas.bind(f'<Shift-B{button}-Motion>',
                                 bind(scroll))
        else:
            self.canvas.bind('<Shift-B1-Motion>',
                             bind(scroll))

        self.win.bind('<MouseWheel>', bind(scroll_event))
        self.win.bind('<Key>', bind(scroll))
        self.win.bind('<Shift-Key>', bind(scroll, 'shift'))
        self.win.bind('<Control-Key>', bind(scroll, 'ctrl'))

    def update_status_line(self, text):
        self.status.config(text=text)

    def run(self):
        MainWindow.run(self)

    def click(self, name):
        self.callbacks[name]()

    def clear(self):
        self.canvas.delete(tk.ALL)

    def update(self):
        self.canvas.update_idletasks()

    def circle(self, color, selected, *bbox):
        if selected:
            outline = '#004500'
            width = 3
        else:
            outline = 'black'
            width = 1
        self.canvas.create_oval(*tuple(int(x) for x in bbox), fill=color,
                                outline=outline, width=width)

    def arc(self, color, selected, start, extent, *bbox):
        if selected:
            outline = '#004500'
            width = 3
        else:
            outline = 'black'
            width = 1
        self.canvas.create_arc(*tuple(int(x) for x in bbox),
                               start=start,
                               extent=extent,
                               fill=color,
                               outline=outline,
                               width=width)

    def line(self, bbox, width=1):
        self.canvas.create_line(*tuple(int(x) for x in bbox), width=width,
                                fill='black')

    def text(self, x, y, txt, anchor=tk.CENTER, color='black'):
        anchor = {'SE': tk.SE}.get(anchor, anchor)
        self.canvas.create_text((x, y), text=txt, anchor=anchor, fill=color)

    def after(self, time, callback):
        id = self.win.after(int(time * 1000), callback)
        # Quick'n'dirty object with a cancel() method:
        return namedtuple('Timer', 'cancel')(lambda: self.win.after_cancel(id))


def bind_enter(widget, callback):
    """Preferred incantation for binding Return/Enter.

    Bindings work differently on different OSes.  This ensures that
    keypad and normal Return work the same on Linux particularly."""

    widget.bind('<Return>', callback)
    widget.bind('<KP_Enter>', callback)
