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

# Copyright 2014 Calculate Ltd. 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 calculate.lib.utils.tools import SavableIterator, ignore
from itertools import ifilter
from HTMLParser import HTMLParser
import re


class BaseConverter(object):
    """
    Базовый класс обработки (ничего не конвертирует - возвращает как есть)
    """
    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=lambda: None):
            self.action = action
            self.condition = condition

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

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

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

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

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

        def tryParse(self, code):
            return code >= self.begin and code <= self.end

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

    def __init__(self, output=None, escSymb="\033"):
        self.output = output or BaseOutput()
        self.escSymb = escSymb
        self.escBlock = (r"{esc}(?:\[(\d+(?:;\d+)*)m|"
                         "\]\d+;.*?\x07|\[\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: "\r" in code or "\n" in code,
                          self.output.newLine)
        tab = element(lambda code: "\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 = SavableIterator([])
        for gram in ifilter(lambda x: x.tryParse(code),
                            self.grams):
            return gram.parse(code, codes)

    def transform(self, s):
        """
        Запустить преобразование текста
        """

        def generator():
            for ctrl, other, txt, _s in self.reParse.findall(s):
                if ctrl:
                    codes = SavableIterator(ctrl.split(';'))
                    for code in codes:
                        code = int(code)
                        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(list(filter(None, generator())))

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


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

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

        def tryParse(self, code):
            return code == self.begin

        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:
                        self.action(colorMap[code])
                    else:
                        self.action(ConsoleColor256.consoleToRgb(code))
            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=None):
        Tags = XmlFormat.Tags
        FontAttr = XmlFormat.FontAttributes
        self.output = output or BaseOutput()
        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(list(filter(None, self.__outdata)))

    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)
