# -*- 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 xml.etree import ElementTree as ET

from lxml import etree as ET
from .palette import (TextState, BaseColorMapping,
                     ConsoleCodeMapping, LightColorMapping, ConsoleColor256,
                     ConsoleCodesInfo, SpanPalette, XmlFormat)
import re

class BaseOutput():
    """
    Базовый вывод текста.

    Вывод просто текста без изменения шрифта
    """

    def __init__(self, state=None):
        pass

    def setBold(self):
        """
        Выводимый текст будет жирным
        """
        pass

    def resetBold(self):
        """
        Выводимый текст будет нежирным
        """
        pass

    def setUnderline(self):
        """
        Выводимый текст будет подчеркнутым
        """
        pass

    def resetUnderline(self):
        """
        Выводимый текст не будет подчеркнутым
        """
        pass

    def setHalfbright(self):
        """
        Цвет выводимого текста использует полутона
        """
        pass

    def resetHalfbright(self):
        """
        Цвет выводимого текста не использует полутона
        """
        pass

    def reset(self):
        """
        Использовать шрифт по умолчанию
        """
        pass

    def endText(self):
        """
        Обработка текста завершена
        """
        return ""

    def outputText(self, text):
        """
        Вывести текст с установленными настройками
        """
        return text

    def setForeground(self, color):
        """
        Установить цвет шрифта
        """
        pass

    def setBackground(self, color):
        """
        Установить цвет фона
        """
        pass

    def resetForeground(self):
        """
        Использовать цвет шрифта по умолчанию
        """
        pass

    def resetBackground(self):
        """
        Использовать цвет фона по умолчанию
        """
        pass

    def setInvert(self):
        """
        Включить инверсию
        """

    def resetInvert(self):
        """
        Выключить инверсию
        """

    def pushState(self):
        """
        Сохранить состояние текста
        """

    def popState(self):
        """
        Восстановить состояние текста из стека
        """

    def newLine(self):
        """
        Вывести текст на новой строке
        """
        return "\n"

    def tab(self):
        """
        Вывести символ табуляции
        """
        return "\t"

    def clone(self):
        """
        Создать копию объекта
        """
        return self.__class__()


class SaveAttrOutput(BaseOutput):
    """
    Базовый класс с сохранением атрибутов
    """

    def __init__(self, state=None):
        super().__init__()
        self.prev_state = state.clone() if state else TextState()
        self.current_state = self.prev_state.clone()
        self.state_stack = []

    def clone(self):
        obj = self.__class__()
        obj.current_state = self.current_state.clone()
        obj.state_stack = list(self.state_stack)
        return obj

    def setBold(self):
        self.current_state.bold = True

    def resetBold(self):
        self.current_state.bold = False

    def setUnderline(self):
        self.current_state.underline = True

    def resetUnderline(self):
        self.current_state.underline = False

    def setHalfbright(self):
        self.current_state.halfbright = True

    def resetHalfbright(self):
        self.current_state.halfbright = False

    def reset(self):
        self.resetBold()
        self.resetHalfbright()
        self.resetUnderline()
        self.resetForeground()
        self.resetBackground()
        self.resetInvert()

    def setForeground(self, color):
        self.current_state.foreground = color

    def resetForeground(self):
        self.current_state.foreground = TextState.Colors.DEFAULT

    def setBackground(self, color):
        self.current_state.background = color

    def resetBackground(self):
        self.current_state.background = TextState.Colors.DEFAULT

    def resetInvert(self):
        self.current_state.invert = False

    def setInvert(self):
        self.current_state.invert = True

    def pushState(self):
        self.state_stack.append(self.current_state.clone())

    def popState(self):
        self.current_state = self.state_stack.pop()


class ColorTerminalOutput(SaveAttrOutput):
    """
    Форматирует текст для вывода в консоль
    """
    mapColors = ConsoleCodeMapping(ConsoleCodesInfo.FOREGROUND,
                                   BaseColorMapping)
    mapLightColors = ConsoleCodeMapping(ConsoleCodesInfo.FOREGROUND -
                                        LightColorMapping.offset,
                                        LightColorMapping)
    mapBackgroundColors = ConsoleCodeMapping(ConsoleCodesInfo.BACKGROUND,
                                             BaseColorMapping)

    def __init__(self, state=None, escSymb=None):
        SaveAttrOutput.__init__(self, state=state)
        if escSymb is None:
            self.escSymb = '\033'
        else:
            self.escSymb = escSymb

    def setBold(self):
        self.resetHalfbright()
        super().setBold()

    def setHalfbright(self):
        self.resetBold()
        super().setHalfbright()

    def handleForeground(self, prevstate, curstate, attrs, tail_attrs):
        """Обработать изменение тона"""
        cci = ConsoleCodesInfo
        color = curstate.foreground
        if color in self.mapColors:
            attrs.append(self.mapColors[color])
        elif color in self.mapLightColors:
            if prevstate.bold == curstate.bold and not curstate.bold:
                tail_attrs.append(cci.NORMAL)
                attrs.append(cci.BOLD)
            attrs.append(self.mapLightColors[color])
        else:
            attrs.append(cci.FOREGROUND_DEFAULT)
            self.resetForeground()

    def handleBackground(self, prevstate, curstate, attrs, tail_attrs):
        """Обработать изменение фона"""
        color = curstate.background
        if color in self.mapBackgroundColors:
            attrs.append(self.mapBackgroundColors[color])
        else:
            attrs.append(ConsoleCodesInfo.BACKGROUND_DEFAULT)

    def handleIntensity(self, prevstate, curstate, attrs, tail_attrs):
        """Обработать интенсивность"""
        if curstate.bold and curstate.bold != prevstate.bold:
            attrs.append(ConsoleCodesInfo.BOLD)
        elif (curstate.halfbright and
                      prevstate.halfbright != curstate.halfbright):
            attrs.append(ConsoleCodesInfo.HALFBRIGHT)
        else:
            attrs.append(ConsoleCodesInfo.NORMAL)

    def handleUnderline(self, prevstate, curstate, attrs, tail_attrs):
        """Обработать подчеркивание"""
        if curstate.underline:
            attrs.append(ConsoleCodesInfo.UNDERLINE)
        else:
            attrs.append(ConsoleCodesInfo.NOUNDERLINE)

    def handleInvert(self, prevstate, curstate, attrs, tail_attrs):
        """Обработать инверсию"""
        if curstate.invert:
            attrs.append(ConsoleCodesInfo.INVERT)
        else:
            attrs.append(ConsoleCodesInfo.NOINVERT)

    def handleColor(self, prevstate, curstate, attrs, tail_attrs):
        """Обработать изменение цветов (фон и/или тон)"""
        if prevstate.foreground != curstate.foreground:
            self.handleForeground(prevstate, curstate, attrs, tail_attrs)
        if prevstate.background != curstate.background:
            self.handleBackground(prevstate, curstate, attrs, tail_attrs)

    def _createAttrs(self, prevstate, curstate):
        """
        Создать ESC последовательность для установки параметров текста
        """
        attrs, tail_attrs = [], []
        # получить интенсивность (полутон и жирность относятся к интенсивности)
        intensity = lambda x: x & (TextState.Attributes.HALFBRIGHT |
                                   TextState.Attributes.BOLD)

        if (prevstate.attr != curstate.attr and
                    curstate.attr == TextState.Attributes.NONE and
                    curstate.foreground is TextState.Colors.DEFAULT and
                    curstate.background is TextState.Colors.DEFAULT):
            attrs.append(ConsoleCodesInfo.RESET)
        else:
            if intensity(prevstate.attr) != intensity(curstate.attr):
                self.handleIntensity(prevstate, curstate, attrs, tail_attrs)
            if prevstate.underline != curstate.underline:
                self.handleUnderline(prevstate, curstate, attrs, tail_attrs)
            if prevstate.invert != curstate.invert:
                self.handleInvert(prevstate, curstate, attrs, tail_attrs)
            if (prevstate.foreground != curstate.foreground or
                        prevstate.background != curstate.background):
                self.handleColor(prevstate, curstate, attrs, tail_attrs)
        return attrs, tail_attrs

    def _createEscCode(self, attrs):
        """
        Создать ESC строку
        """
        attrs = [str(x) for x in ['%s[' % self.escSymb] + attrs + ['m']]
        return "%s%s%s" % (attrs[0], ";".join(attrs[1:-1]), attrs[-1])

    def outputText(self, s):
        """
        Задание параметров текста и вывод его
        """
        if self.prev_state != self.current_state:
            attr, tail_attrs = \
                self._createAttrs(self.prev_state, self.current_state)
            attr = self._createEscCode(attr)
            if tail_attrs:
                postattr = self._createEscCode(tail_attrs)
            else:
                postattr = ""
            self.prev_state = self.current_state.clone()
        else:
            attr = ""
            postattr = ""
        return attr + s + postattr

    def endText(self):
        self.reset()
        return self.outputText("")

    def newLine(self):
        return "\n"

    def tab(self):
        return "\t"


class ColorTerminal256Output(ColorTerminalOutput):
    """
    Вывод на 256 цветный терминал
    """
    mapLightColors = LightColorMapping.mapTS_Console

    def handleForeground(self, prevstate, curstate, attrs, tail_attrs):
        color = curstate.foreground
        color256 = ConsoleColor256.rgbToConsole(color)
        if not color256 and color in self.mapLightColors:
            color256 = self.mapLightColors[color]
        if color256:
            attrs.extend([ConsoleCodesInfo.FOREGROUND256,
                          ConsoleCodesInfo.COLOR256,
                          color256])
        else:
            super().handleForeground(prevstate, curstate, attrs, tail_attrs)

    def handleBackground(self, prevstate, curstate, attrs, tail_attrs):
        color = curstate.background
        color256 = ConsoleColor256.rgbToConsole(color)
        if not color256 and color in self.mapLightColors:
            color256 = self.mapLightColors[color]
        if color256:
            attrs.extend([ConsoleCodesInfo.BACKGROUND256,
                          ConsoleCodesInfo.COLOR256,
                          color256])
        else:
            super().handleBackground(prevstate, curstate, attrs, tail_attrs)


class ColorTerminal16Output(ColorTerminalOutput):
    """
    Вывод на 16 цветный терминал с преобразованием RGB к ближайшему базовому

    Bugs:
    После преобразования текст и фон могут одинакового цвета
    """

    def __init__(self, state=None, palette=None, escSymb=None):
        ColorTerminalOutput.__init__(self, state=state, escSymb=escSymb)
        self.palette = palette

    def _handleNearestColors(self, color):
        """
        Обработка преобразования к ближайшему цвету
        """
        standardColors = TextState.normalColors + TextState.lightColors
        if self.palette and color not in standardColors:
            return self.palette.getBaseColorByRGB(color)
        return color

    def handleForeground(self, prevstate, curstate, attrs, tail_attrs):
        """
        Добавить преобразование RGB к ближайшему базовому
        """
        _curstate = curstate.clone()
        _curstate.foreground = \
            self._handleNearestColors(curstate.foreground)
        super().handleForeground(
            prevstate, _curstate,
            attrs, tail_attrs)

    def handleBackground(self, prevstate, curstate, attrs, tail_attrs):
        """
        Добавить преобразование RGB к ближайшему базовому
        """
        mapHighNormal = dict(zip(TextState.lightColors,
                                 TextState.normalColors))
        _curstate = curstate.clone()
        _curstate.background = \
            self._handleNearestColors(curstate.background)
        # преобразовать яркий цвет к обычному
        _curstate.background = \
            mapHighNormal.get(_curstate.background, _curstate.background)
        super().handleBackground(
            prevstate, _curstate,
            attrs, tail_attrs)


class SpanCssOutput(SaveAttrOutput):
    """
    Форматирует текст для вывода в консоль
    """

    def __init__(self, state=None, palette=SpanPalette()):
        SaveAttrOutput.__init__(self, state=state)
        self.palette = palette

    def getStringColor(self, color, bold=False, halfbright=False,
                       background=False):
        """
        Получить название цвета по номеру и состоянию текста
        """
        if halfbright:
            bright = SpanPalette.LOW_BRIGHT
        elif bold:
            bright = SpanPalette.HIGH_BRIGHT
        else:
            bright = SpanPalette.NORMAL_BRIGHT

        if background:
            return self.palette.getBackgroundColor(color)
        else:
            return self.palette.getTextColor(color, bright)

    def getTags(self, prevstate, curstate):
        """
        Создать tag span для указания параметров текста
        """
        style = []

        colorAttr = ["color", "background"]
        if curstate.invert:
            colorAttr = colorAttr[1], colorAttr[0]
        if (prevstate.foreground != curstate.foreground or
                    prevstate.bold != curstate.bold or
                curstate.invert or
                    prevstate.halfbright != curstate.halfbright):
            sColor = self.getStringColor(curstate.foreground,
                                         curstate.bold,
                                         curstate.halfbright,
                                         background=False)
            style.append("%s:%s;" % (colorAttr[0], sColor))
        if prevstate.background != curstate.background or curstate.invert:
            sColor = self.getStringColor(curstate.background,
                                         background=True)
            style.append("%s:%s;" % (colorAttr[1], sColor))
        if prevstate.underline != curstate.underline:
            if curstate.underline:
                style.append("text-decoration:underline;")
            else:
                style.append("text-decoration:none;")
        if prevstate.bold != curstate.bold:
            if curstate.bold:
                style.append("font-weight:bold;")
            else:
                style.append("font-weight:normal;")
        return '<span style="%s">' % "".join(style), '</span>'

    def outputText(self, s):
        if self.prev_state != self.current_state:
            lattr, rattr = self.getTags(self.prev_state, self.current_state)
        else:
            lattr = rattr = ""
        return lattr + XmlFormat.escaper(s) + rattr

    def endText(self):
        self.reset()
        return ""

    def newLine(self):
        return "<br/>"

    def tab(self):
        return "&#9;"


class XmlOutput(SaveAttrOutput):
    """
    Форматирует текст c описанием формата в XML для внутренней передачи

    Bugs: игнорирует первоначальное состояние (state)
          не экономное использование тэгов (например при выводе нескольких строк
    """
    escaper = XmlFormat.escaper

    def __init__(self, state=None):
        super().__init__(state=state)
        self.clear_state = TextState()

    def getXML(self, curstate, text):
        """
        Создать управляющие тэги

        :type curstate: TextState
        :type text: str
        """
        Tags, FontAttributes = XmlFormat.Tags, XmlFormat.FontAttributes

        root = ET.Element("root")
        tail = root

        if (curstate.foreground != TextState.Colors.DEFAULT or
                    curstate.background != TextState.Colors.DEFAULT):
            tail = ET.SubElement(tail, Tags.FONT)
            sColor = str(curstate.foreground or "")
            if sColor:
                tail.attrib[FontAttributes.FOREGROUND] = sColor
            sColor = str(curstate.background or "")
            if sColor:
                tail.attrib[FontAttributes.BACKGROUND] = sColor
        if curstate.halfbright:
            tail = ET.SubElement(tail, Tags.HALFBRIGHT)
        if curstate.invert:
            tail = ET.SubElement(tail, Tags.INVERT)
        if curstate.underline:
            tail = ET.SubElement(tail, Tags.UNDERLINE)
        if curstate.bold:
            tail = ET.SubElement(tail, Tags.BOLD)
        #TODO debug here
        text = re.sub('[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]+', '', text)
        tail.text = text
        # return root[0]
        return root

    def xmlToString(self, xml):
        """
        Получить строку xml не преобразовывая &, <, >
        """

        def generator(root):
            if root.attrib:
                yield "<%s %s>" % (root.tag,
                                   " ".join(['%s="%s"' % (k, v) for k, v in
                                             root.attrib.items()]))
            else:
                #py3 compat :(
                if root.tag != "root":
                    yield "<%s>" % root.tag
            if len(root):
                for element in root:
                    for text in generator(element):
                        yield text
            if root.text:
                yield self.escaper(root.text)
            if root.tag != "root":
                yield "</%s>" % root.tag

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

    def outputText(self, s):
        if self.clear_state != self.current_state:
            return self.xmlToString(self.getXML(self.current_state, s))
        else:
            return self.escaper(s)

    def endText(self):
        self.reset()
        return ""

    def newLine(self):
        return "<br/>"

    def tab(self):
        return "<tab/>"


class BasePositionOutput():
    """
    Объект составляющий ESC последовательности для управлением местом вывода
    """

    def moveCursorUp(self, count=1):
        """
        Переместить курсор вверх
        """
        return ""

    def moveCursorDown(self, count=1):
        """
        Переместить курсор вниз
        """
        return ""

    def moveCursorRight(self, count=1):
        """
        Переместить курсор вправо
        """
        return ""

    def moveCursorLeft(self, count=1):
        """
        Переместить курсор влево
        """
        return ""

    def clearLine(self, whole_line=False):
        """
        Очистить строку от курсора до конца или всю строку
        """
        return ""

    def savePosition(self):
        """
        Сохранить положение курсора
        """
        return ""

    def restorePosition(self):
        """
        Восстановить положение курсора
        """
        return ""


class TerminalPositionOutput(BasePositionOutput):
    """
    Управление позицией вывода текста в терминале
    """

    class Codes():
        UP = 'A'
        DOWN = 'B'
        RIGHT = 'C'
        LEFT = 'D'
        CLEAR_LINE = 'K'
        CLEAR_FROM_CURSOR = '1'
        CLEAR_WHOLE_LINE = '2'
        SAVE_POSITION = 's'
        RESTORE_POSITION = 'u'

    def _createEscCode(self, attrs):
        """
        Создать ESC строку
        """
        return '\033[%s' % attrs

    def _moveCursor(self, direct, count):
        if int(count) > 1:
            count = str(count)
        else:
            count = ""
        return self._createEscCode("%s%s" % (count, direct))

    def moveCursorDown(self, count=1):
        return self._moveCursor(self.Codes.DOWN, count)

    def moveCursorUp(self, count=1):
        return self._moveCursor(self.Codes.UP, count)

    def moveCursorRight(self, count=1):
        return self._moveCursor(self.Codes.RIGHT, count)

    def moveCursorLeft(self, count=1):
        return self._moveCursor(self.Codes.LEFT, count)

    def clearLine(self, whole_line=False):
        if whole_line:
            mode_code = self.Codes.CLEAR_WHOLE_LINE
        else:
            mode_code = self.Codes.CLEAR_FROM_CURSOR
        return self._createEscCode("%s%s" % (mode_code, self.Codes.CLEAR_LINE))

    def savePosition(self):
        return self._createEscCode(self.Codes.SAVE_POSITION)

    def restorePosition(self):
        return self._createEscCode(self.Codes.RESTORE_POSITION)
