import glob
import os
from abc import ABC, abstractmethod

import cells.utility as utility
from cells import events
from cells.observation import Observation
from cells.settings import ApplicationInfo, Settings
from PySide2.QtCore import Qt, QUrl
from PySide2.QtGui import QKeySequence
from PySide2.QtWebEngineWidgets import QWebEnginePage, QWebEngineView
from PySide2.QtWidgets import QAction, QShortcut, QSizePolicy


class CodeView(Observation, QWebEngineView):
    def __init__(self, subject, delegate=None, viewTip=True):
        self.delegate = delegate
        self.viewTip = viewTip

        Observation.__init__(self, subject)
        QWebEngineView.__init__(self)
        self.settings = Settings(subject)
        self.settings.open()

        page = Ace(delegate, subject, self)
        self.setPage(page)
        self.setMinimumSize(500, 300)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

        self.setContextMenuPolicy(Qt.NoContextMenu)
        self.setWindowModality(Qt.ApplicationModal)

        self.page().loadFinished.connect(self.onLoadFinished)
        self.setWindowFlags(Qt.Tool | Qt.WindowTitleHint
                            | Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint
                            | Qt.WindowMaximizeButtonHint)
        self.setWindowFlag(Qt.WindowMinimizeButtonHint, False)

        self.closeShortcut = QShortcut(QKeySequence(self.tr("Ctrl+w")), self,
                                       self.close)

        self.add_responder(events.settings.Save, self.settingsSaveResponder)

    def onLoadFinished(self, ok):
        self.loadDelegate()
        self.loadSettings()

    def settingsSaveResponder(self, e):
        self.settings = e.settings
        self.loadSettings()

    def setDelegate(self, delegate):
        self.delegate = delegate
        self.page().delegate = delegate
        self.setWindowTitle(self.delegate.codeWindowTitle())
        self.loadDelegate()

    def loadDelegate(self):
        if self.delegate is None:
            return

        if len(self.delegate.code()) < 1 and self.viewTip:
            self.setCodeAsync(self.tip())
            self.page().runJavaScript("editor.selectAll();")
        else:
            self.setCodeAsync(self.delegate.code())
        self.page().runJavaScript("editor.focus();")

    def setTheme(self, name):
        aceName = self.asAceProperty(name)
        self.page().runJavaScript(f"editor.setTheme('ace/theme/{aceName}');")

    def setKeybindings(self, name):
        aceName = self.asAceProperty(name)
        self.page().runJavaScript(
            f"editor.setKeyboardHandler('ace/keyboard/{aceName}');")

    def setMode(self, name):
        aceName = self.asAceProperty(name)
        self.page().runJavaScript(
            f"editor.session.setMode('ace/mode/{aceName}');")

    def asAceProperty(self, name):
        return name.lower().replace(" ", "_")

    def setCodeAsync(self, code):
        self.page().runJavaScript(f"editor.session.setValue({repr(code)});")

    def postContent(self):
        self.page().runJavaScript(
            f"console.log('{Token.getContent}' + editor.getValue());")

    def tip(self):
        return "Shift+Enter    - evaluate line or selection\n" +\
               "Ctrl/Cmd+Enter - evaluate the whole buffer\n" +\
               "Ctrl/Cmd+W     - close the editor\n" +\
               "Ctrl/Cmd+Alt+H - view all shortcuts"

    def loadSettings(self):
        self.setTheme(self.settings['editor'].get('theme'))
        self.setKeybindings(self.settings['editor'].get('keybindings'))

    def delete(self):
        self.page().unregister()
        self.page().setParent(None)
        self.page().deleteLater()
        self.unregister()
        self.setParent(None)
        self.deleteLater()

    def closeEvent(self, event):
        self.postContent()


class Ace(Observation, QWebEnginePage):
    def __init__(self, delegate, subject, parentView):
        self.delegate = delegate
        self.parentView = parentView

        QWebEnginePage.__init__(self)
        Observation.__init__(self, subject)

        aceUrl = QUrl.fromLocalFile(
            os.path.join(utility.viewResourcesDir(), "ace", "index.html"))
        self.load(aceUrl)
        # I'd like to move all the python<->js communication here
        # but when I'm starting to implement something more than just
        # virtual functions, I get:
        # ERROR:mach_port_broker.mm(193)] Unknown process  is sending Mach IPC messages!
        # so here is implementations of callbacks only

    def javaScriptConsoleMessage(self, level, message, lineNumber, sourceID):
        self.parseConsoleOutput(message)

    def parseConsoleOutput(self, message):
        if self.delegate is None:
            return

        if message.startswith(Token.evaluate):
            self.evaluate(message[len(Token.evaluate):])
        elif message.startswith(Token.getContent):
            self.getContent(message[len(Token.getContent):])

    def evaluate(self, code):
        if self.delegate:
            self.notify(
                events.view.code.Evaluate(self.delegate.template(), code))

    def getContent(self, content):
        if self.delegate is not None and content != self.parentView.tip():
            self.delegate.setCode(content, True)


class Token:
    evaluate = "<-!code_evaluation_triggered!->"
    getContent = "<-!get_content_triggered!->"


class CodeDelegate(ABC):
    @abstractmethod
    def setCode(self, code, notify):
        pass

    @abstractmethod
    def code(self):
        return ""

    @abstractmethod
    def codeWindowTitle(self):
        return ""

    @abstractmethod
    def template(self):
        pass


def aceSrcDir():
    return os.path.join(utility.viewResourcesDir(), "ace", "ace-builds", "src")


def acePropertyNames(prefix, postfix, title=True):
    themesDir = aceSrcDir()
    names = sorted(glob.glob(os.path.join(themesDir, prefix + "*" + postfix)))
    names = [os.path.basename(name) for name in names]
    names = [
        name[len(prefix):-len(postfix)].replace("_", " ") for name in names
    ]

    if title:
        names = [name.title() for name in names]

    return names
