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

# Copyright 2015-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.

import hashlib

import os
from os import path

import re
import sys
from calculate.lib.utils.colortext.palette import TextState
from calculate.lib.utils.tools import ignore
from calculate.lib.utils.git import Git, GitError
from calculate.lib.utils.portage import (EmergePackage, PackageList,
                                         EmergeUpdateInfo,
                                         EmergeRemoveInfo)

Colors = TextState.Colors
import pexpect
from calculate.lib.utils.files import (getProgPath, readLinesFile,
                                       listDirectory,
                                       writeFile, readFile)
from calculate.lib.utils.colortext.output import XmlOutput
from calculate.lib.utils.colortext.converter import (ConsoleCodes256Converter,
                                                     XmlConverter)
from calculate.lib.cl_log import log

from calculate.lib.cl_lang import setLocalTranslate, getLazyLocalTranslate, _

setLocalTranslate('cl_update3', sys.modules[__name__])
__ = getLazyLocalTranslate(_)

linux_term_env = {'TERM': 'linux'}

class EmergeError(Exception):
    """
    Ошибка при сборке пакетов
    """


class EmergeNeedRootError(EmergeError):
    pass


class CommandExecutor():
    """
    Запуск программы для объекта Emerge
    """
    logfile = '/var/log/calculate/lastcommand.log'

    def __init__(self, cmd, params, env=None, cwd=None, logfile=None):
        self.cwd = cwd
        self.env = env or dict(os.environ)
        self.env.update({'EINFO_QUIET': 'NO'})
        self.env.update(linux_term_env)
        self.cmd = cmd
        self.params = params
        self.child = None
        if logfile:
            self.logfile = logfile

    def get_command(self):
        return [self.cmd] + list(self.params)

    #TODO make sure pexpect encoding doesn't mess up anything
    #if it does, instead put log file as binary maybe?
    def execute(self):
        if self.child is None:
            command_data = self.get_command()
            self.child = pexpect.spawn(command_data[0], command_data[1:],
                                       logfile=open(self.logfile, 'w'),
                                       env=self.env, cwd=self.cwd, timeout=None,
                                       encoding="UTF-8", codec_errors='ignore')
        return self.child

    def close(self):
        if self.child is not None:
            try:
                self.child.close()
            except pexpect.ExceptionPexpect:
                self.child.close(force=True)
            self.child = None

    def success(self):
        if self.child:
            self.child.read()
            if self.child.isalive():
                self.child.wait()
            return self.child.exitstatus == 0
        return False

    def failed(self):
        return not self.success()

    def send(self, s):
        if self.child:
            self.child.send(s)


class EmergeCommand(CommandExecutor):
    """
    Запуск emerge для последующего анализирования
    """
    # параметры по умолчанию
    default_params = ["-av", "--color=y", "--nospinner"]
    emerge_cmd = getProgPath("/usr/bin/emerge")

    def __init__(self, packages, extra_params=None, env=None, cwd=None,
                 logfile=None, emerge_default_opts=None, env_update=None,
                 use=""):
        extra_params = extra_params or []
        if env is None:
            if emerge_default_opts is None:
                env = {'CLEAN_DELAY': '0'}
            else:
                env = {
                    'CLEAN_DELAY': '0',
                    'EMERGE_DEFAULT_OPTS': re.sub(
                        r'(?:^|\s)(--columns)(?=\s|$)', '',
                        emerge_default_opts)
                }
                if use:
                    env["USE"] = use
            env.update(os.environ)
        if env_update is not None:
            env.update(env_update)

        params = self.default_params + extra_params + packages
        super().__init__(self.emerge_cmd, params=params,
                                            env=env, cwd=cwd, logfile=logfile)


def Chroot(chroot_path, obj):
    """
    Преобразовать команду (экземпляр объекта) в chroot
    :param obj: экземпляр команды
    :param chroot_path: путь для chroot
    :return:
    """
    old_get_command = obj.get_command

    def get_command():
        chrootCmd = '/usr/bin/chroot'
        bashCmd = '/bin/bash'
        bash_command = (
            "env-update &>/dev/null;"
            "source /etc/profile &>/dev/null;"
            "{cmd}".format(cmd=" ".join(old_get_command())))
        return [chrootCmd, chroot_path, bashCmd, "-c", bash_command]
    obj.get_command = get_command
    return obj


def Linux32(obj):
    """
    Преобразовать команду (экземпляр объекта) в вызов под linux32
    :param obj: экземпляр команды
    :return:
    """
    old_get_command = obj.get_command

    def get_command():
        return ["/usr/bin/linux32"] + old_get_command()
    obj.get_command = get_command
    return obj


class InfoBlockInterface():
    """
    Интерфейс для информационного блока
    """
    action = None
    token = None
    result = None
    text_converter = ConsoleCodes256Converter(XmlOutput())

    def get_block(self, child):
        pass

    def add_element(self, element):
        """
        :type element: InfoBlockInterface
        """
        pass


class EmergeInformationBlock(InfoBlockInterface):
    _color_block = "(?:\033\[[^m]+?m)?"
    _new_line = "(?:\r*\n)"
    end_token = ["\n"]
    re_block = None
    re_match_type = type(re.match("", ""))
    re_type = type(re.compile(""))

    def __init__(self, parent):
        """
        :type parent: InfoBlockInterface
        """
        self.result = None
        self.text_converter = parent.text_converter
        self.parent = parent
        self.parent.add_element(self)
        self.children = []

    def add_element(self, element):
        """
        :type element: InfoBlockInterface
        """
        self.children.append(element)

    def __str__(self):
        if type(self.result) == self.re_match_type:
            return self.result.group()
        else:
            return self.result or ""

    def __nonzero__(self):
        return bool(self.result)

    def __len__(self):
        if self.result is None:
            return 0
        else:
            return len(self.result)

    def __contains__(self, item):
        if self.result is None:
            return False
        else:
            return item in str(self)

    def _get_text(self, result):
        """
        Получить результат из регулярки и преобразовать его через self.converter
        """
        if result:
            return self.text_converter.transform(result.rstrip())
        return ""

    def get_block(self, child):
        try:
            token = child.match
            if type(self.end_token) == self.re_type:
                child.expect(self.end_token)
                match = child.match.group()
            else:
                child.expect_exact(self.end_token)
                match = child.match
            self.get_data(self.re_block.search(
                token + child.before + match))
        except pexpect.EOF:
            child.buffer = "".join(
                [x for x in (child.before, child.after, child.buffer)
                 if type(x) == str])

    def get_data(self, match):
        self.result = self._get_text(match.group(1))


class InstallPackagesBlock(EmergeInformationBlock):
    """
    Блок emerge содержащий список пакетов для установки
    """
    list = PackageList([])
    remove_list = PackageList([])
    block_packages = False
    _new_line = EmergeInformationBlock._new_line
    _color_block = EmergeInformationBlock._color_block
    token = "\n["
    end_token = ["\r\n\r", "\n\n"]

    re_block = re.compile(r"((?:^\[.*?{nl})+)".format(nl=_new_line),
                          re.MULTILINE)
    re_blocks = re.compile(r"\[{c}blocks{c} {c}b".format(c=_color_block))

    def get_data(self, match):
        super().get_data(match)
        list_block = XmlConverter().transform(self.result).split('\n')
        self.list = PackageList(map(EmergeUpdateInfo, list_block))
        self.remove_list = PackageList(map(EmergeRemoveInfo, list_block))
        self.block_packages = any(self.re_blocks.search(x) for x in list_block)


class UninstallPackagesBlock(EmergeInformationBlock):
    """
    Блок emerge содержащий список удаляемых пакетов
    """
    list = PackageList([])
    verbose_result = ""
    _new_line = EmergeInformationBlock._new_line
    _color_block = EmergeInformationBlock._color_block
    token = ["Calculating removal order",
             "These are the packages that would be unmerged"]
    end_token = re.compile("All selected packages:.*\n")
    re_block = re.compile(
        r"(?:{token}).*?{nl}(.*){nl}All selected packages: (.*?){nl}".
        format(token="|".join(token),
               nl=_new_line, c=_color_block), re.DOTALL)

    def get_data(self, match):
        re_clean = re.compile(
            "^.*?({token}).*?{c}{nl}".format(token="|".join(self.token),
                                             nl=self._new_line,
                                             c=self._color_block), re.DOTALL)
        verbose_result = re_clean.sub("", match.group(1))
        self.verbose_result = self._get_text(verbose_result)
        self.result = self._get_text(match.group(2))
        list_block = XmlConverter().transform(self.result).split()
        self.list = PackageList(map(EmergePackage, list_block))


class GroupEmergeInformationBlock(EmergeInformationBlock):
    """
    Группа блоков
    """
    def get_block(self, child):
        self.children_get_block(child)
        self.result = True

    def children_get_block(self, child):
        for block in self.children:
            block.get_block(child)

    def children_action(self, child):
        for block in (x for x in self.children if x.result and x.action):
            if block.action(child) is False:
                return False

    def action(self, child):
        self.children_action(child)
        return False


class FinishEmergeGroup(GroupEmergeInformationBlock):
    """
    Блок завершения команды
    """
    token = pexpect.EOF
    # регуляреное выражение, определяющее содержит ли блок
    # сообщения об ошибках
    re_failed = re.compile(
        r"Fetch instructions for \S+:|"
        r"The following.*are necessary to proceed|"
        r"!!! Multiple package .* slot have been pulled|"
        r"no ebuilds to satisfy|"
        r"Dependencies could not be completely resolved due to",
        re.MULTILINE)

    def get_block(self, child):
        if child.isalive():
            child.wait()
        if child.exitstatus != 0 or self.re_failed.search(child.before):
            self.children_get_block(child)
        else:
            self.result = True


class PrepareErrorBlock(EmergeInformationBlock):
    """
    Блок информации с ошибками при получении списка устанавливаемых пакетов
    """
    token = None

    re_drop = re.compile("news items need reading|"
                         "Use eselect news|"
                         "Calculating dependencies|"
                         "to read news items|"
                         "Local copy of remote index is up-to-date|"
                         "These are the packages that would be merged|"
                         "Process finished with exit code")
    re_multi_empty_line = re.compile("(?:<br/>){3,}", re.DOTALL)
    re_strip_br = re.compile("^(?:<br/>)+|(?:<br/>)+$", re.DOTALL)

    def remove_needless_data(self, data):
        return "\n".join([x for x in data.split('\n')
                          if not self.re_drop.search(x)])

    def strip_br(self, data):
        return self.re_strip_br.sub(
            "",
            self.re_multi_empty_line.sub("<br/><br/>", data))

    def get_block(self, child):
        self.result = self.strip_br(
            self._get_text(self.remove_needless_data(child.before)))

    def action(self, child):
        raise EmergeError(_("Emerge failed"))


class DownloadSizeBlock(EmergeInformationBlock):
    """
    Размер скачиваемых обновлений
    """
    token = "Size of downloads:"

    re_block = re.compile(r"Size of downloads:\s(\S+\s\S+)")

    def __str__(self):
        if self.result:
            return self.result
        else:
            return "0 kB"

class SkippedPackagesBlock(EmergeInformationBlock):
    """
    Размер скачиваемых обновлений
    """
    token = "The following update has been skipped"
    end_token = ["For more information, see the MASKED"]

    re_block = re.compile(
        r"(The following update has.*?)(?=For more information)", re.S)

    def __str__(self):
        if self.result:
            return self.result
        else:
            return ""

class QuestionGroup(GroupEmergeInformationBlock):
    """
    Группа блоков разбора вопросов от emerge
    """
    token = "Would you like"
    end_token = ["]", "\n"]
    _color_block = EmergeInformationBlock._color_block
    re_block = re.compile(
        "(Would you.*)\[{c}Yes{c}/{c}No{c}".format(c=_color_block))

    def get_block(self, child):
        try:
            before = child.before
            token = child.match
            if type(self.end_token) == self.re_type:
                child.expect(self.end_token)
                match = child.match.group()
            else:
                child.expect_exact(self.end_token)
                match = child.match
            data = token + child.before + match
            child.before = before
            for block in self.children:
                child.match = re.search(block.token, data)
                block.get_block(child)
                if block.result:
                    break
        except pexpect.EOF:
            child.buffer = "".join(
                [x for x in (child.before, child.after, child.buffer)
                 if type(x) == str])

class QuestionChangeConfigBlock(GroupEmergeInformationBlock):
    """
    Вопрос об изменении конфигурационных файлов
    """
    token = "Would you like to add these changes to your config files"

    def get_block(self, child):
        if child.match:
            self.result = self.token
            self.children_get_block(child)

    def action(self, child):
        if self.result:
            child.send("no\n")
            if child.isalive():
                child.wait()
            self.children_action(child)


class QuestionBlock(EmergeInformationBlock):
    """
    Блок вопроса
    """
    default_answer = "yes"
    token = "Would you"

    def get_block(self, child):
        if child.match:
            self.result = self.token

    def action(self, child):
        if self.result:
            child.send("%s\n" % self.default_answer)
            return False


class NeedRootBlock(EmergeInformationBlock):
    """
    Пользователь не явеляется root
    """
    token = "This action requires superuser access"

    def get_data(self, child):
        self.result = True

    def action(self, child):
        raise EmergeNeedRootError(_("This action requires superuser access"))


class NotifierInformationBlock(EmergeInformationBlock):
    """
    Информационный блок поддерживающий observing
    """
    def __init__(self, parent):
        super().__init__(parent)
        self.observers = []

    def get_data(self, match):
        self.result = match

    def add_observer(self, f):
        self.observers.append(f)

    def clear_observers(self):
        self.observers = []

    def remove_observer(self, f):
        if f in self.observers:
            self.observers.remove(f)

    def notify(self, observer, groups):
        observer(groups)

    def action(self, child):
        if self.result and self.observers:
            groups = self.result.groups()
            for observer in self.observers:
                self.notify(observer, groups)


class EmergingPackage(NotifierInformationBlock):
    """
    Запуск устанавливаемого пакета

    ObserverFunc: (package, num=number, max_num=number, binary=binary)
    """
    _color_block = EmergeInformationBlock._color_block
    token = ">>> Emerging "
    re_block = re.compile(
        "Emerging (binary )?\({c}(\d+){c} "
        "of {c}(\d+){c}\) {c}([^\s\033]+){c}".format(c=_color_block))

    def notify(self, observer, groups):
        observer(EmergePackage(groups[3]), num=groups[1], max_num=groups[2],
                 binary=bool(groups[0]))


class UnemergingPackage(NotifierInformationBlock):
    """
    Запуск устанавливаемого пакета

    ObserverFunc: (package, num=number, max_num=number)
    """
    _color_block = EmergeInformationBlock._color_block
    token = ">>> Unmerging"
    re_block = re.compile(
        r"Unmerging (?:\({c}(\d+){c} "
        r"of {c}(\d+){c}\) )?(\S+)\.\.\.".format(c=_color_block))

    def notify(self, observer, groups):
        observer(EmergePackage(groups[2]), num=groups[0], max_num=groups[1])


class FetchingTarball(NotifierInformationBlock):
    """
    Происходит скачивание архивов
    """
    token = "Saving to:"
    re_block = re.compile("Saving to:\s*[‘'](\S+)?['’]")

    def notify(self, observer, groups):
        observer(groups[0])


class InstallingPackage(NotifierInformationBlock):
    """
    Запуск устанавливаемого пакета

    ObserverFunc: (package, binary=binary)
    """
    _color_block = EmergeInformationBlock._color_block
    binary = None

    token = ">>> Installing "
    re_block = re.compile(
        "Installing \({c}(\d+){c} "
        "of {c}(\d+){c}\) {c}([^\s\033]+){c}".format(c=_color_block))

    def notify(self, observer, groups):
        strpkg = str(EmergePackage(groups[2]))
        binary = bool(self.binary and strpkg in self.binary)
        observer(EmergePackage(groups[2]), binary=binary)

    def mark_binary(self, package):
        if self.binary is None:
            self.binary = []
        self.binary.append(str(package))


class EmergeingErrorBlock(EmergeInformationBlock):
    """
    Блок содержит информацию об ошибке во время сборки пакета
    """
    token = ["* ERROR: ", " * \033[39;49;00mERROR: "]
    end_token = "Working directory:"
    re_block = re.compile("ERROR: (\S*) failed \([^)]+\).*?"
                          "The complete build log is located at '([^']+)",
                          re.DOTALL)
    package = ""

    def get_data(self, match):
        self.result = self._get_text(match.group(2).rstrip())
        self.package = match.group(1)

    @property
    def log(self):
        return self.text_converter.transform(readFile(self.result))

    def action(self, child):
        raise EmergeError(_("Failed to emerge %s") % self.package)


class RevdepPercentBlock(NotifierInformationBlock):
    """
    Блок определния статуса revdep-rebuild
    """
    token = "Collecting system binaries"
    end_token = [re.compile("Assigning files to packages|"
                            "All prepared. Starting rebuild")]
    re_block = re.compile("\[\s(\d+)%\s\]")
    action = None

    def notify(self, observer, groups):
        percent = int(groups[0])
        observer(percent)

    def get_block(self, child):
        expect_result = [self.re_block]+self.end_token
        try:
            while True:
                index = child.expect(expect_result)
                if index == 0:
                    for observer in self.observers:
                        self.notify(observer, child.match.groups())
                else:
                    self.result = child.match
                    break
        except pexpect.EOF:
            self.result = ""

class EmergeParser(InfoBlockInterface):
    """
    Парсер вывода emerge
    """

    def __init__(self, command, run=False):
        self.command = command
        self.elements = {}

        self.install_packages = InstallPackagesBlock(self)
        self.uninstall_packages = UninstallPackagesBlock(self)
        self.question_group = QuestionGroup(self)
        self.change_config_question = QuestionChangeConfigBlock(
            self.question_group)
        self.question = QuestionBlock(self.question_group)
        self.finish_block = FinishEmergeGroup(self)
        self.need_root = NeedRootBlock(self)
        self.prepare_error = PrepareErrorBlock(self.finish_block)
        self.change_config_question.add_element(self.prepare_error)
        self.download_size = DownloadSizeBlock(self)
        self.skipped_packages = SkippedPackagesBlock(self)
        self.emerging_error = EmergeingErrorBlock(self)

        self.installing = InstallingPackage(self)
        self.uninstalling = UnemergingPackage(self)
        self.emerging = EmergingPackage(self)
        self.fetching = FetchingTarball(self)

        self.emerging.add_observer(self.mark_binary)
        self.emerging.add_observer(self.skip_fetching)
        if run:
            self.run()

    def mark_binary(self, package, binary=False, **kw):
        if binary:
            self.installing.mark_binary(package)

    def skip_fetching(self, *argv, **kw):
        self.fetching.action = lambda child: None

    def add_element(self, element):
        """
        :type element: InfoBlockInterface
        """
        if element.token:
            if type(element.token) == list:
                for token in element.token:
                    self.elements[token] = element
            else:
                self.elements[element.token] = element

    def run(self):
        """
        Запустить команду
        """
        child = self.command.execute()

        while True:
            index = child.expect_exact(self.elements.keys())
            element = list(self.elements.values())[index]
            element.get_block(child)
            if element.action:
                if element.action(child) is False:
                    break

    def close(self):
        self.command.close()

    def __enter__(self):
        return self

    def __exit__(self, *exc_info):
        self.close()


class MtimeCheckvalue():
    def __init__(self, *fname):
        self.fname = fname

    def value_func(self, fn):
        return str(int(os.stat(fn).st_mtime))

    def get_check_values(self, file_list):
        for fn in file_list:
            if path.exists(fn) and not path.isdir(fn):
                yield fn, self.value_func(fn)
            else:
                for k, v in self.get_check_values(
                        listDirectory(fn, fullPath=True)):
                    yield k, v

    def checkvalues(self):
        return self.get_check_values(self.fname)


class Md5Checkvalue(MtimeCheckvalue):
    def value_func(self, fn):
        return hashlib.md5(readFile(fn, binary=True)).hexdigest()


class GitCheckvalue():
    def __init__(self, git, rpath):
        self.rpath = rpath
        self.git = git

    def checkvalues(self):
        with ignore(GitError):
            if self.git.is_git(self.rpath):
                yield self.rpath, self.git.getCurrentCommit(self.rpath)


class EmergeCache():
    """
    Кэш пакетов
    """
    cache_file = '/var/lib/calculate/calculate-update/world.cache'
    # список файлов проверяемый по mtime на изменения
    check_list = [MtimeCheckvalue('/etc/make.conf',
                                  '/etc/portage',
                                  '/etc/make.profile'),
                  Md5Checkvalue('/var/lib/portage/world',
                                '/var/lib/portage/world_sets')]
    logger = log("emerge-cache",
                 filename="/var/log/calculate/emerge-cache.log",
                 formatter="%(asctime)s - %(levelname)s - %(message)s")

    def __init__(self):
        self.files_control_values = {}
        self.pkg_list = PackageList([])

    def set_cache(self, package_list):
        """
        Установить в кэш список пакетов
        """
        with writeFile(self.cache_file) as f:
            for fn, val in self.get_control_values().items():
                f.write("{fn}={val}\n".format(fn=fn, val=val))
            f.write('\n')
            for pkg in package_list:
                f.write("%s\n" % str(pkg))
        self.logger.info("Setting cache (%d packages)" % len(package_list))

    def drop_cache(self, reason=None):
        if path.exists(self.cache_file):
            with ignore(OSError):
                os.unlink(self.cache_file)
            self.logger.info("Droping cache. Reason: %s" % reason)
        else:
            self.logger.info("Droping empty cache. Reason: %s" % reason)

    def get_cached_package_list(self):
        self.read_cache()
        if not path.exists(self.cache_file):
            self.logger.info("Requesting empty cache")
        if self.check_actuality():
            return self.pkg_list
        return None

    def check_actuality(self):
        """
        Кэш считается актуальным если ни один из файлов не менялся
        """
        if self.get_control_values() == self.files_control_values:
            self.logger.info(
                "Getting actuality cache (%d packages)" % len(self.pkg_list))
            return True
        else:
            reason = "Unknown"
            for k, v in self.get_control_values().items():
                if k in self.files_control_values:
                    if v != self.files_control_values[k]:
                        reason = "%s was modified" % k
                else:
                    reason = "Checksum of file %s is not exist" % k
            self.logger.info("Failed to get cache. Reason: %s" % reason)
            return False

    def read_cache(self):
        self.files_control_values = {}
        cache_file_lines = readLinesFile(self.cache_file)
        for line in cache_file_lines:
            if "=" not in line:
                break
            k, v = line.split('=')
            self.files_control_values[k] = v.strip()
        self.pkg_list = PackageList(cache_file_lines)

    def get_control_values(self):
        def generate():
            for obj in self.check_list:
                for check_value in obj.checkvalues():
                    yield check_value

        return dict(generate())
