import logging
import tkinter as tk
from functools import cached_property
from pathlib import Path
from tkinter import messagebox, ttk
from typing import NamedTuple

import voicemeeterlib
from voicemeeterlib import kinds

from .builders import MainFrameBuilder
from .configurations import loader
from .data import _base_values, _configuration, get_configuration
from .errors import VMCompactError
from .menu import Menus
from .subject import Subject

logger = logging.getLogger(__name__)


class App(tk.Tk):
    """App mainframe"""

    @classmethod
    def make(cls, kind: NamedTuple):
        """
        Factory function for App.

        Returns an App class of a kind.
        """

        APP_cls = type(
            f'Voicemeeter{kind}.Compact',
            (cls,),
            {
                'kind': kind,
            },
        )
        return APP_cls

    def __init__(self, vmr, theme):
        super().__init__()
        self.logger = logger.getChild(self.__class__.__name__)
        self._vmr = vmr
        self._vmr.event.add(['pdirty', 'ldirty'])
        self.subject = Subject()
        self.start_updates()
        self._vmr.init_thread()
        for pn in (
            Path(__file__).parent.resolve() / 'img' / 'cat.ico',
            Path.cwd() / '_internal' / 'img' / 'cat.ico',
        ):
            if pn.is_file():
                self.iconbitmap(str(pn))
                break
        self.minsize(275, False)
        self._configs = None
        self.protocol('WM_DELETE_WINDOW', self.on_close_window)
        self.menu = self['menu'] = Menus(self, vmr)
        self.styletable = ttk.Style()
        if _configuration.config:
            vmr.apply_config(_configuration.config)

        self.build_app()

        self.drag_id = ''
        self.bind('<Configure>', self.dragging)

        self.after(1, self.healthcheck_step)

    def __str__(self):
        return f'{type(self).__name__}App'

    @property
    def target(self):
        """returns the current interface"""

        return self._vban if _base_values.vban_connected else self._vmr

    @property
    def configframes(self):
        """returns the current configframes"""

        return (
            frame
            for frame in self.winfo_children()
            if isinstance(frame, ttk.Frame)
            and '!stripconfig' in str(frame)
            or '!busconfig' in str(frame)
        )

    def build_app(self, kind=None, vban=None):
        """builds the app frames according to a kind"""
        self._vban = vban
        if kind:
            self.kind = kind

        # register event callbacks
        self.target.subject.add([self.on_pdirty, self.on_ldirty])

        self.bus_frame = None
        self.submix_frame = None
        self.builder = MainFrameBuilder(self)
        self.builder.setup()
        self.builder.create_channelframe('strip')
        self.builder.create_separator()
        self.builder.create_navframe()
        if _configuration.extended:
            self.nav_frame.extend.set(True)
            self.nav_frame.extend_frame()
        if self.kind.name == 'potato':
            self.builder.create_banner()

    def on_pdirty(self):
        if _base_values.run_update:
            self.after(1, self.subject.notify, 'pdirty')

    def on_ldirty(self):
        if not _base_values.dragging:
            self.after(1, self.subject.notify, 'ldirty')

    def _destroy_top_level_frames(self):
        """
        Clear observables.

        Deregister app as observer.

        Destroy all top level frames.
        """
        self.target.subject.remove([self.on_pdirty, self.on_ldirty])
        self.subject.clear()
        [
            frame.destroy()
            for frame in self.winfo_children()
            if isinstance(frame, ttk.Frame)
        ]

    def dragging(self, event, *args):
        if event.widget is self:
            if self.drag_id == '':
                _base_values.dragging = True
            else:
                self.after_cancel(self.drag_id)
            self.drag_id = self.after(100, self.stop_drag)

    def stop_drag(self):
        self.drag_id = ''
        _base_values.dragging = False

    @cached_property
    def userconfigs(self):
        self._configs = loader(self.kind.name, self.target)
        return self._configs

    def start_updates(self):
        def init():
            self.logger.debug('updates started')
            _base_values.run_update = True

        if self._vmr.gui.launched_by_api:
            self.subject.notify('pdirty')
            self.after(12000, init)
        else:
            init()

    def healthcheck_step(self):
        if not _base_values.vban_connected:
            try:
                self._vmr.version
            except voicemeeterlib.error.CAPIError:
                resp = messagebox.askyesno(message='Restart Voicemeeter GUI?')
                if resp:
                    self.logger.debug(
                        'healthcheck failed, rebuilding the app after GUI restart.'
                    )
                    self._vmr.end_thread()
                    self._vmr.run_voicemeeter(self._vmr.kind.name)
                    _base_values.run_update = False
                    self._vmr.init_thread()
                    self.after(8000, self.start_updates)
                    self._destroy_top_level_frames()
                    self.build_app(self._vmr.kind)
                    vban_config = get_configuration('vban')
                    for i, _ in enumerate(vban_config):
                        target = getattr(self.menu, f'menu_vban_{i + 1}')
                        target.entryconfig(0, state='normal')
                        target.entryconfig(1, state='disabled')
                    [
                        self.menu.menu_vban.entryconfig(j, state='normal')
                        for j, _ in enumerate(self.menu.menu_vban.winfo_children())
                    ]
                else:
                    self.destroy()
        self.after(250, self.healthcheck_step)

    def on_close_window(self):
        if _base_values.vban_connected:
            self._vban.logout()
        self.destroy()


_apps = {kind.name: App.make(kind) for kind in kinds.all}


def connect(kind_id: str, vmr, theme=None) -> App:
    """return App of the kind requested"""

    try:
        VMMIN_cls = _apps[kind_id]
    except KeyError:
        raise VMCompactError(f'Invalid kind: {kind_id}')
    return VMMIN_cls(vmr, theme)
