# -*- coding: utf-8 -*-

# Copyright 2014-2016 Mir Calculate. http://www.calculate-linux.org
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

from .output import BaseOutput
from .palette import (TextState, BaseColorMapping, ConsoleCodesInfo,
                     LightColorMapping, ConsoleColor256, XmlFormat)
from ..tools import SaveableIterator, ignore
from html.parser import HTMLParser
# from HTMLParser import HTMLParser
import re


class BaseConverter():
    """
    Базовый класс обработки (ничего не конвертирует - возвращает как есть)
    """

    def __init__(self, output=BaseOutput()):
        self.output = output

    def transform(self, s):
        return self.output.outputText(s)

    def detect(self, s):
        return True


class ConsoleCodesConverter(BaseConverter):
    """Преобразователь текста из цветного консольного вывода через объект
    форматирования.

    Объект форматирования должен реализовывать BaseOutput

    >>> cct = ConsoleCodesConverter(BaseOutput())
    >>> outtext = "\033[32;1mHello\033[0;39m"
    >>> cct.transform(outtext)
    'Hello'
    >>> from output import SpanCssOutput
    >>> cct = ConsoleCodesConverter(SpanCssOutput())
    >>> outtext = "\033[32;1mHello\033[0;39m"
    >>> cct.transform(outtext)
    '<span style="color:Green;font-weight:bold;">Hello</span>'
    """

    class CodeElement():
        """Элемент кода в ESC последовательности"""

        def __init__(self, condition=lambda code: False, action=None):
            self.action = action
            self.condition = condition

        def tryParse(self, code):
            """Обрабатывает ли экземпляр код"""
            return self.condition(code)

        def parse(self, code, codes):
            """Обработать код, вызвать действие"""
            if callable(self.action):
                return self.action()
            return None

        def _next_code(self, other):
            """
            Получить следующий код
            """
            try:
                return int(next(other))
            except StopIteration:
                return None

    class ColorElement():
        """Элемент кода для указания стандартного цвета
        
        Проверка кода в интервале, запуск действия с передачей цвета
        """
        # соответствие консольных цветов внутренним цветам
        mapColors = BaseColorMapping.mapConsole_TS

        def __init__(self, action=None, begin=None, end=None):
            self.action = action
            self.begin = begin
            self.end = end

        def tryParse(self, code):
            if not isinstance(code, int):
                return False
            return self.begin <= code <= self.end

        def parse(self, code, codes):
            if callable(self.action):
                return self.action(self.mapColors.get(code - self.begin,
                                                      TextState.Colors.DEFAULT))
            else:
                return None

    def __init__(self, output=BaseOutput(), escSymb="\033"):
        super().__init__(output)
        self.escSymb = escSymb
        self.escBlock = (r"{esc}(?:\[(\d*(?:;\d+)*)(m)|"
                         "\]\d+;.*?\x07|"
                         "\([B0UK]|"
                         "\[\d*[A-D])".format(esc=escSymb))
        self.otherSymb = "(?:\r*\n|\t)"
        self.reEscBlock = re.compile(self.escBlock)
        self.reParse = re.compile(
            "(?:{0}|({1}))?(.*?)(?=$|{0}|{1})".format(self.escBlock,
                                                      self.otherSymb),
            re.DOTALL)
        resetBoldHalfbright = lambda: (
            (self.output.resetBold() or "") +
            (self.output.resetHalfbright() or ""))
        cci = ConsoleCodesInfo
        element = self.CodeElement

        # набор правил обработки кодов
        reset = element(lambda code: code == cci.RESET, self.output.reset)
        bold = element(lambda code: code == cci.BOLD, self.output.setBold)
        halfbright = element(lambda code: code == cci.HALFBRIGHT,
                             self.output.setHalfbright)
        underline = element(lambda code: code == cci.UNDERLINE,
                            self.output.setUnderline)
        nounderline = element(lambda code: code == cci.NOUNDERLINE,
                              self.output.resetUnderline)
        invert = element(lambda code: code == cci.INVERT,
                         self.output.setInvert)
        noinvert = element(lambda code: code == cci.NOINVERT,
                           self.output.resetInvert)
        normal = element(lambda code: code == cci.NORMAL,
                         resetBoldHalfbright)
        reset_foreground = element(lambda code: code == cci.FOREGROUND_DEFAULT,
                                   self.output.resetForeground)
        reset_background = element(lambda code: code == cci.BACKGROUND_DEFAULT,
                                   self.output.resetBackground)
        foreground = self.ColorElement(begin=cci.FOREGROUND,
                                       end=cci.FOREGROUND_END,
                                       action=self.output.setForeground)
        background = self.ColorElement(begin=cci.BACKGROUND,
                                       end=cci.BACKGROUND_END,
                                       action=self.output.setBackground)
        newline = element(
            lambda code: type(code) != int and ("\r" in code or "\n" in code),
            self.output.newLine)
        tab = element(lambda code: type(code) != int and "\t" in code,
                      self.output.tab)
        self.grams = [reset, bold, halfbright, underline, nounderline, normal,
                      invert, noinvert, reset_foreground, reset_background,
                      foreground, background, tab, newline]

    def evaluteGram(self, code, codes=None):
        """Выполнить грамматику"""
        if codes is None:
            codes = SaveableIterator([])
        for gram in (x for x in self.grams if x.tryParse(code)):
            return gram.parse(code, codes)

    def transform(self, s):
        """
        Запустить преобразование текста
        """
        def generator():
            for ctrl, m, other, txt, _s, _m in self.reParse.findall(s):
                if m:
                    codes = SaveableIterator(ctrl.split(';'))
                    for code in codes:
                        code = int(code or '0')
                        res = self.evaluteGram(code, codes)
                        if res:
                            yield res
                elif other:
                    res = self.evaluteGram(other)
                    if res:
                        yield res
                if txt:
                    yield self.output.outputText(txt)
            yield self.output.endText()

        return "".join(x for x in generator() if x)

    def detect(self, s):
        """
        Определить есть ли в тексте управляющие последовательности
        """
        return bool(self.reEscBlock.search(s))


class ConsoleCodes256Converter(ConsoleCodesConverter):
    """Расширяет возможность обработки 256 цветного терминала"""

    class Color256Element(ConsoleCodesConverter.CodeElement):
        def __init__(self, action=None, begin=None):
            super().__init__(condition=lambda code: code == begin,
                                 action=action)

        def parse(self, code, codes):
            """
            Тон: 38;5;0-255
            Фон: 48;5;0-255
            """
            colorMap = LightColorMapping(BaseColorMapping).mapConsole_TS

            codes.save()
            if self._next_code(codes) == ConsoleCodesInfo.COLOR256:
                code = self._next_code(codes)
                if code is not None:
                    if code in colorMap:
                        var = colorMap[code]
                    else:
                        var = ConsoleColor256.consoleToRgb(code)
                    if callable(self.action):
                        self.action(var)
            else:
                # если после 38 не 5 - не обрабатываем этот код
                codes.restore()

    def __init__(self, *args, **kwargs):
        ConsoleCodesConverter.__init__(self, *args, **kwargs)
        cci = ConsoleCodesInfo
        # обработчики кодов для вывода в 256
        foreground256 = self.Color256Element(begin=cci.FOREGROUND256,
                                             action=self.output.setForeground)
        background256 = self.Color256Element(begin=cci.BACKGROUND256,
                                             action=self.output.setBackground)
        self.grams.insert(0, foreground256)
        self.grams.insert(0, background256)


class XmlConverter(BaseConverter):
    """
    Преобразователь текста из внутреннего xml формата
    """
    unescaper = XmlFormat.unescaper

    def __init__(self, output=BaseOutput()):
        super().__init__(output)
        Tags = XmlFormat.Tags
        FontAttr = XmlFormat.FontAttributes
        self.tagMap = {
            Tags.BOLD: self.output.setBold,
            Tags.HALFBRIGHT: self.output.setHalfbright,
            Tags.INVERT: self.output.setInvert,
            Tags.UNDERLINE: self.output.setUnderline,
            Tags.FONT: self.parseFont
        }
        self.singletagMap = {
            Tags.NEWLINE: self.output.newLine,
            Tags.TAB: self.output.tab
        }
        self.colorMap = {FontAttr.FOREGROUND.lower(): self.output.setForeground,
                         FontAttr.BACKGROUND.lower(): self.output.setBackground}
        self.reMatch = re.compile("<(?:%s)" % "|".join(self.tagMap.keys()),
                                  re.I)
        self.parser = self.createParser()

    def createParser(self):
        """
        Создать парсер HTML кода
        """
        parser = HTMLParser()
        parser.handle_starttag = self.startElementHandler
        parser.handle_endtag = self.endElementHandler
        parser.handle_data = self.characterDataHandler
        parser.handle_startendtag = self.startendElementHandler
        parser.handle_entityref = self.entityrefElementHandler
        return parser

    def parseFont(self, *attrs):
        for k, v in attrs:
            k = str(k).lower()
            if k in self.colorMap:
                self.colorMap[k](str(v))

    def transform(self, s):
        self.__outdata = []
        self.__tagStack = []
        self.parser.feed(s)
        self.__outdata.append(self.output.endText())
        return "".join((x for x in self.__outdata if x))
        

    def addResultToOutdata(f):
        """Добавить возвращаемый результат в список self.__outdata"""

        def wrapper(self, *args):
            res = f(self, *args)
            if res:
                self.__outdata.append(res)
            return res

        return wrapper

    def _buildTaq(self, name, attrs=(), startendTag=False, endTag=False):
        """
        Создать тэг по параметрам
        """
        lslash, rslash = '', ''
        if startendTag:
            rslash = '/'
        elif endTag:
            lslash = '/'
        if attrs:
            return "<{name} {attrs}{rslash}>".format(
                name=name, attrs=" ".join(['%s="%s"' % (k, v)
                                           for k, v in attrs]),
                rslash=rslash)
        else:
            return "<{lslash}{name}{rslash}>".format(lslash=lslash, name=name,
                                                     rslash=rslash)

    @addResultToOutdata
    def startElementHandler(self, name, attrs):
        """Обработчик начального тега"""
        if name in self.tagMap:
            self.output.pushState()
            self.__tagStack.append(name)
            with ignore(TypeError):
                return self.tagMap[name](*attrs)
        else:
            return self.output.outputText(self._buildTaq(name, attrs))

    @addResultToOutdata
    def startendElementHandler(self, name, attrs):
        """Обработчик одиночного тега"""
        if name in self.singletagMap:
            with ignore(TypeError):
                return self.singletagMap[name](*attrs)
        else:
            return self.output.outputText(
                self._buildTaq(name, attrs, startendTag=True))

    @addResultToOutdata
    def endElementHandler(self, name):
        """Обработчик завершающего тега"""
        if name in self.tagMap:
            if name in self.__tagStack:
                while self.__tagStack and self.__tagStack.pop() != name:
                    self.output.popState()
                self.output.popState()
        else:
            return self.output.outputText(self._buildTaq(name, endTag=True))

    @addResultToOutdata
    def characterDataHandler(self, data):
        """Обработчик текста в тэгах"""
        return self.output.outputText(self.unescaper(data))

    @addResultToOutdata
    def entityrefElementHandler(self, data):
        return self.output.outputText(self.unescaper("&%s;" % data))

    def detect(self, s):
        return bool(self.reMatch.search(s))

    addResultToOutdata = staticmethod(addResultToOutdata)
