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

# Copyright 2008-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 re
import sys
from os import path
import time
import datetime
import functools
import bz2

from calculate.lib.cl_xml import ET
import pexpect
from calculate.lib.configparser import ConfigParser

from calculate.lib.utils.colortext.palette import TextState
from calculate.lib.utils.common import cmpVersion
from calculate.lib.utils.files import (getProgPath, find, process,
                                       readLinesFile, pathJoin)
from calculate.lib.utils.tools import SavableIterator, ignore
from collections import Mapping, defaultdict
from common import getTupleVersion
from contextlib import closing
from files import listDirectory, readFile
from functools import total_ordering
from itertools import ifilter, imap, chain

Colors = TextState.Colors
import glob

from calculate.lib.cl_lang import setLocalTranslate

setLocalTranslate('cl_lib3', sys.modules[__name__])

reVerSplit = re.compile(r"^(?:.*/var/db/pkg/)?(?:(\w+-\w+)/)?(.*?)-(([^-]+?)"
                        "(?:-(r\d+))?)(?:.(tbz2))?$", re.S)

def reVerSplitToPV(x):
    """Convert match from reVerSplit to PV hash"""
    if type(x) in (str, unicode):
        x = reVerSplit.search(x)
    if x:
        match = x.groups()
        return {'CATEGORY': match[0] or "",
                'PN': match[1],
                'PF': "%s-%s" % (match[1], match[2]),
                'P': "%s-%s" % (match[1], match[3]),
                'PV': match[3],
                'PR': match[4] or "r0",
                'PVR': match[2]}
    return {'PN': "",
            'PF': "",
            'P': "",
            'PV': "",
            'PR': "",
            'PVR': ""}.copy()


def getPkgUses(fullpkg, version=None, prefix="/"):
    """Get USE and IUSE from package"""
    category, slash, pkg = fullpkg.partition('/')
    _pkgCategory = 'var/db/pkg/{0}'.format(category)
    pkgCategory = path.join(prefix, _pkgCategory)
    if version is None:
        packages = filter(lambda x: x['PN'] == pkg,
                          map(reVerSplitToPV,
                              filter(lambda x: x,
                                     map(lambda x: reVerSplit.search(x),
                                         listDirectory(pkgCategory)))))
        if not packages:
            return None
        usePath = path.join(pkgCategory, packages[-1]['PF'], "USE")
        iusePath = path.join(pkgCategory, packages[-1]['PF'], "IUSE")
    else:
        usePath = path.join(pkgCategory, "%s-%s" % (pkg, version), "USE")
        iusePath = path.join(pkgCategory, "%s-%s" % (pkg, version), "IUSE")
    iuse = readFile(iusePath).strip().split()
    use = readFile(usePath).strip().split()
    return (map(lambda x: x[1:] if x.startswith("+") else x,
                filter(lambda x: x,
                       use)),
            map(lambda x: x[1:] if x.startswith("+") else x,
                filter(lambda x: x,
                       iuse)))


def isPkgInstalled(pkg, prefix='/', sortByVersion=False):
    """Check is package installed"""
    pkgDir = path.join(prefix, 'var/db/pkg')
    if "/" in pkg:
        category, op, pkgname = pkg.partition('/')

        def package_generator(pkg_dn):
            for dn in listDirectory(pkg_dn, fullPath=True):
                pkg_info = reVerSplitToPV(dn)
                if pkg_info['PN'] == pkgname:
                    pkg_info.update({'CATEGORY': category})
                    yield pkg_info
        res = list(package_generator(path.join(pkgDir, category)))
        if len(res) > 1 and sortByVersion:
            return sorted(res, key=lambda x: getTupleVersion(x['PVR']))
        else:
            return res
    else:
        return filter(lambda x: filter(lambda y: y['PN'] == pkg,
                                       map(reVerSplitToPV,
                                           listDirectory(x))),
                      listDirectory(pkgDir, fullPath=True))


def getPkgSlot(pkg, prefix='/'):
    """Get package slot"""
    pkgs = isPkgInstalled(pkg, prefix)
    pkgDir = path.join(prefix, 'var/db/pkg')
    return [readFile(path.join(pkgDir, x['CATEGORY'],
                               x['PF'], "SLOT")).strip()
            for x in pkgs]


def getPkgActiveUses(fullpkg):
    """Get active uses from package"""
    res = getPkgUses(fullpkg)
    if not res:
        return None
    return list(set(res[0]) & set(res[1]))


def getSquashList():
    """Get supprted squashfs compressions method"""
    wantMethod = {"lzo", "lzma", "xz", "gzip"}
    squashfs_tools = "sys-fs/squashfs-tools"
    usesSquashFs = getPkgActiveUses(squashfs_tools)
    if not usesSquashFs:
        return ["gzip"]
    else:
        pkgInfo = isPkgInstalled(squashfs_tools)
        if pkgInfo and pkgInfo[0].get('PV', None):
            pkgVer = getTupleVersion(pkgInfo[0].get('PV'))
            gzipVer = getTupleVersion('4.2')
            if pkgVer >= gzipVer:
                usesSquashFs.append('gzip')
        return list(set(usesSquashFs) & wantMethod)


class RepositorySubstituting(object):
    """
    Объект выполняющий подставнку repository:path пути

    так как для подстановки необходимо получить все пути репозиториев, и это
    занимает время, то объек извлекает данные из переменной только при
    необходимости: в строке найдено repository:
    """
    token = re.compile("^\w+:")

    def __init__(self, dv, system_root=''):
        self.dv = dv
        self._substitution = None
        self.system_root = system_root.rstrip("/")

    @classmethod
    def has_repos_name(cls, s):
        return bool(cls.token.search(s))

    def _prepare_substitution(self):
        # TODO: проверка выхода за chroot
        emerge_config = self.dv.Get('cl_emerge_config')
        if emerge_config and emerge_config.repositories:
            repos = {x.name.encode('utf-8'): x.location.encode('utf-8')
                     for x in emerge_config.repositories}
            r = re.compile("|".join("^%s:" % x for x in repos.keys()))
            self._substitution = (
                functools.partial(
                    r.sub, lambda *args: "%s%s/profiles/" % (
                        self.system_root, repos.get(
                            args[0].group(0)[:-1], ""))))
        else:
            self._substitution = lambda x: x

    def __call__(self, s):
        if self.has_repos_name(s):
            if not self._substitution:
                self._prepare_substitution()
            return self._substitution(s)
        return s


def searchProfile(dirpath, configname, repository_sub=None):
    """Search profile"""

    def search(dirpath):
        parentpath = path.join(dirpath, "parent")
        if path.exists(parentpath):
            for line in open(parentpath, 'r'):
                if repository_sub:
                    line = repository_sub(line)
                search_dn = path.realpath(path.join(dirpath, line.strip()))
                for dn in search(search_dn):
                    yield dn
        fullconfig = path.join(dirpath, configname)
        if path.exists(fullconfig):
            yield fullconfig

    return list(search(dirpath))


class Layman:
    """
    Объект для управления репозиториями Layman

    Args:
        installed: путь до installed.xml
        laymanconf: путь до layman.conf
        makeconf: путь до makeconf
    """
    layman_package = "app-portage/layman"
    portage_package = "sys-apps/portage"

    def __init__(self, installed, makeconf, laymanconf=None, prefix="/"):
        self.installed = installed
        self.makeconf = makeconf
        self.laymanconf = laymanconf
        self.prefix = prefix

    def _add_to_laymanconf(self, rname, rurl, rpath):
        if not self.laymanconf:
            return
        config = ConfigParser(strict=False)
        config.read(self.laymanconf, encoding="utf-8")
        if not config.has_section(rname):
            config.add_section(rname)
        for k, v in {'auto-sync': 'Yes',
                     'layman-type': 'git',
                     'priority': '50',
                     'sync-uri': rurl,
                     'sync-type': 'laymansync',
                     'location': rpath}.items():
            config.set(rname, k, v)
        with open(self.laymanconf, 'wb') as f:
            config.write(f)

    def _remove_from_laymanconf(self, rname):
        if not self.laymanconf:
            return
        config = ConfigParser(strict=False)
        config.read(self.laymanconf, encoding="utf-8")
        if config.has_section(rname):
            config.remove_section(rname)
            with open(self.laymanconf, 'wb') as f:
                config.write(f)

    def _add_to_installed(self, rname, rurl):
        """
        Добавить репозиторий в installed.xml
        """
        if path.exists(self.installed) and readFile(self.installed).strip():
            tree = ET.parse(self.installed)
            root = tree.getroot()
            # если репозиторий уже присутсвует в installed.xml
            if root.find("repo[name='%s']" % rname) is not None:
                return
        else:
            root = ET.Element("repositories", version="1.0")
            tree = ET.ElementTree(root)

        newrepo = ET.SubElement(root, "repo", priority="50",
                                quality="experimental",
                                status="unofficial")
        name = ET.SubElement(newrepo, "name")
        name.text = rname
        source = ET.SubElement(newrepo, "source", type="git")
        source.text = rurl
        try:
            from layman.utils import indent
        except ImportError:
            indent = None
        if indent:
            indent(root)
        with open(self.installed, 'w') as f:
            f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
            tree.write(f, encoding="utf-8")

    portdir_param = "PORTDIR_OVERLAY"

    def _add_to_makeconf(self, rpath):
        """
        Добавить репозиторий в layman/make.conf
        """

        def fixContent(match):
            repos = match.group(1).strip().split('\n')
            if not rpath in repos:
                repos.insert(0, rpath)
            return '%s="\n%s"' % (self.portdir_param, "\n".join(repos))

        if path.exists(self.makeconf):
            content = readFile(self.makeconf)
            if self.portdir_param in content:
                new_content = re.sub('\A%s="([^\"]+)"' % self.portdir_param,
                                     fixContent, content, re.DOTALL)
                if new_content == content:
                    return
                else:
                    content = new_content
            else:
                content = '%s="\n%s"\n' % (self.portdir_param, rpath + content)
        else:
            content = '%s="\n%s"\n' % (self.portdir_param, rpath)
        with open(self.makeconf, 'w') as f:
            f.write(content)

    def _remove_from_makeconf(self, rpath):
        """
        Удалить путь из make.conf
        """

        def fixContent(match):
            repos = match.group(1).strip().split('\n')
            if rpath in repos:
                repos.remove(rpath)
            return '%s="\n%s"' % (self.portdir_param, "\n".join(repos))

        if path.exists(self.makeconf):
            content = readFile(self.makeconf)
            if self.portdir_param in content:
                new_content = re.sub('\A%s="([^\"]+)"' % self.portdir_param,
                                     fixContent, content, re.DOTALL)
                if new_content == content:
                    return
                with open(self.makeconf, 'w') as f:
                    f.write(new_content)

    def is_new_layman(self):
        layman_pkg_info = isPkgInstalled(self.layman_package,
                                         prefix=self.prefix.encode('utf-8'))
        portage_pkg_info = isPkgInstalled(self.portage_package,
                                          prefix=self.prefix.encode('utf-8'))
        if layman_pkg_info and portage_pkg_info:
            layman_ver = layman_pkg_info[0].get('PV')
            portage_ver = portage_pkg_info[0].get('PV')
            if (cmpVersion(layman_ver, "2.3") >= 0 and
                        cmpVersion(portage_ver, "2.2.18") >= 0):
                return True
        return False

    def add(self, rname, rurl, rpath):
        """
        Добавить репозиторий в installed.xml и layman/make.conf
        """
        self._add_to_installed(rname, rurl)
        if self.is_new_layman():
            self._add_to_laymanconf(rname, rurl, rpath)
        else:
            self._add_to_makeconf(rpath)
        return True

    def _remove_from_installed(self, rname):
        """
        Удалить репозиторий в installed.xml
        """
        if path.exists(self.installed) and readFile(self.installed).strip():
            tree = ET.parse(self.installed)
            root = tree.getroot()
            el = root.find("repo[name='%s']" % rname)
            if el is not None:
                root.remove(el)
                try:
                    from layman.utils import indent
                except ImportError:
                    indent = None
                if indent:
                    indent(root)
                with open(self.installed, 'w') as f:
                    f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
                    tree.write(f, encoding="utf-8")

    def remove(self, rname, rpath):
        """
        Удалить репозиторий из настроек layman
        """
        self._remove_from_installed(rname)
        if self.is_new_layman():
            self._remove_from_laymanconf(rname)
        else:
            self._remove_from_makeconf(rpath)
        return True

    def get_installed(self):
        """
        Получить список установленных репозиториев
        """
        if path.exists(self.installed) and readFile(self.installed).strip():
            tree = ET.parse(self.installed)
            return [x.text for x in tree.findall("repo/name")]
        return []


@total_ordering
class EmergePackage(Mapping):
    """
    Данные о пакете

    Item keys: CATEGORY, P, PN, PV, P, PF, PR, PVR

    Поддерживает сравнение объекта с другим таким же объектом по версии, либо
    со строкой, содержащей версию. Сравнение выполняется по категория/имя, затем
    по версии
    """
    default_repo = 'gentoo'
    prefix = r"(?:.*/var/db/pkg/|=)?"
    category = r"(?:(\w+(?:-\w+)?)/)?"
    pn = "([^/]*?)"
    pv = r"(?:-(\d[^-]*?))?"
    pr = r"(?:-(r\d+))?"
    tbz = r"(?:.(tbz2))?"
    slot = r'(?::(\w+(?:\.\w+)*(?:/\w+(?:\.\w+)*(?:-\w+(?:\.\w+)*)?)?))?'
    repo = r'(?:::(\w+))?'

    reParse = re.compile(
        r'^{prefix}{category}(({pn}{pv}){pr}){slot}{repo}{tbz}$'.format(
            prefix=prefix, category=category, pn=pn, pv=pv, pr=pr, tbz=tbz,
            slot=slot, repo=repo))

    attrs = ('CATEGORY', 'PN', 'PF', 'SLOT', 'REPO', 'P', 'PV', 'PR', 'PVR',
             'CATEGORY/PN')

    def _parsePackageString(self, s):
        """
        Преобразовать строка в части названия пакета
        """
        x = self.reParse.search(s)
        if x:
            CATEGORY, PF, P, PN, PV, PR, SLOT, REPO, TBZ = range(0, 9)
            x = x.groups()
            d = {'CATEGORY': x[CATEGORY] or "",
                 'PN': x[PN],
                 'PV': x[PV] or '0',
                 'PF': x[PF],
                 'P': x[P],
                 'SLOT': x[SLOT] or '0',
                 'REPO': x[REPO] or self.default_repo,
                 'CATEGORY/PN': "%s/%s" % (x[CATEGORY], x[PN]),
                 'PR': x[PR] or 'r0'}
            if x[PR]:
                d['PVR'] = "%s-%s" % (d['PV'], d['PR'])
            else:
                d['PVR'] = d['PV']
            if d['PF'].endswith('-r0'):
                d['PF'] = d['PF'][:-3]
            return d.copy()
        else:
            return {k: '' for k in self.attrs}

    def __iter__(self):
        return iter(self.attrs)

    def __len__(self):
        if not self['PN']:
            return 0
        else:
            return len(self.attrs)

    def __lt__(self, version):
        """
        В объектах сравнивается совпадение категории и PF
        """
        if "CATEGORY/PN" in version and "PVR" in version:
            if self['CATEGORY/PN'] < version['CATEGORY/PN']:
                return True
            elif self['CATEGORY/PN'] > version['CATEGORY/PN']:
                return False
            version = "%s-%s" % (version['PV'], version['PR'])
        currentVersion = "%s-%s" % (self['PV'], self['PR'])
        return cmpVersion(currentVersion, version) == -1

    def __eq__(self, version):
        if "CATEGORY" in version and "PF" in version:
            return ("%s/%s" % (self['CATEGORY'], self['PF']) ==
                    "%s/%s" % (version['CATEGORY'], version['PF']))
        else:
            currentVersion = "%s-%s" % (self['PV'], self['PR'])
            return cmpVersion(currentVersion, version) == 0

    def __init__(self, package):
        if isinstance(package, EmergePackage):
            self.__result = package.__result
            self._package = package._package
        else:
            self._package = package
            self.__result = None

    def __getitem__(self, item):
        if not self.__result:
            self.__result = self._parsePackageString(self._package)
        return self.__result[item]

    def __repr__(self):
        return "EmergePackage(%s/%s)" % (self['CATEGORY'], self['PF'])

    def __str__(self):
        return "%s/%s" % (self['CATEGORY'], self['PF'])


class PackageInformation:
    """
    Объект позволяет получать информацию о пакете из eix
    """
    eix_cmd = getProgPath("/usr/bin/eix")

    query_packages = []
    information_cache = defaultdict(dict)

    fields = ["DESCRIPTION"]

    def __init__(self, pkg):
        self._pkg = pkg
        if not pkg in self.query_packages:
            self.query_packages.append(pkg)

    def __getitem__(self, item):
        if not self._pkg['CATEGORY/PN'] in self.information_cache and \
                self.query_packages:
            self.query_information()
        try:
            return self.information_cache[self._pkg['CATEGORY/PN']][item]
        except KeyError:
            return ""

    def query_information(self):
        pkg_list = "|".join(
            [x['CATEGORY/PN'].replace("+", r"\+") for x in self.query_packages])
        try:
            output = pexpect.spawn(self.eix_cmd, ["--xml", pkg_list],
                                   timeout=60).read()
        except pexpect.TIMEOUT:
            output = ""
        re_cut = re.compile("^.*?(?=<\?xml version)", re.S)
        with ignore(ET.ParseError):
            xml = ET.fromstring(re_cut.sub('', output))
            for pkg in self.query_packages:
                cat_pn = pkg['CATEGORY/PN']
                if not cat_pn in self.information_cache:
                    descr_node = xml.find(
                        'category[@name="%s"]/package[@name="%s"]/description'
                        % (pkg['CATEGORY'], pkg['PN']))
                    if descr_node is not None:
                        self.information_cache[cat_pn]['DESCRIPTION'] = \
                            descr_node.text
        while self.query_packages:
            self.query_packages.pop()

    @classmethod
    def add_info(cls, pkg):
        pkg.info = cls(pkg)
        return pkg


class UnmergePackage(EmergePackage):
    """
    Информация об обновлении одного пакета
    """
    re_pkg_info = re.compile("^\s(\S+)\n\s+selected:\s(\S+)", re.MULTILINE)

    def __init__(self, package):
        super(UnmergePackage, self).__init__(package)
        if not isinstance(package, EmergePackage):
            self._package = self.convert_package_info(package)

    def convert_package_info(self, package):
        match = self.re_pkg_info.search(package)
        if match:
            return "%s-%s" % match.groups()
        return ""


def recalculate_update_info(cls):
    """
    Добавить
    """
    cls.update_info = re.compile(
        r"^({install_info})\s+({atom_info})\s*(?:{prev_version})?"
        r"\s*({use_info})?.*?({pkg_size})?$".format(
            install_info=cls.install_info,
            atom_info=cls.atom_info,
            prev_version=cls.prev_version,
            use_info=cls.use_info,
            pkg_size=cls.pkg_size), re.MULTILINE)
    return cls


@recalculate_update_info
@total_ordering
class EmergeUpdateInfo(Mapping):
    """
    Информация об обновлении одного пакета
    """

    install_info = "\[(binary|ebuild)([^\]]+)\]"
    atom_info = r"\S+"
    use_info = 'USE="[^"]+"'
    prev_version = "\[([^\]]+)\]"
    pkg_size = r"[\d,]+ \w+"

    attrs = ['binary', 'REPLACING_VERSIONS', 'SIZE', 'new', 'newslot',
             'updating', 'downgrading', 'reinstall']

    def __init__(self, data):
        self._data = data
        self._package = None
        self._info = {}

    def _parseData(self):
        r = self.update_info.search(self._data)
        if r:
            self._info['binary'] = r.group(2) == 'binary'
            install_flag = r.group(3)
            self._info['newslot'] = "S" in install_flag
            self._info['new'] = "N" in install_flag and not "S" in install_flag
            self._info['updating'] = ("U" in install_flag and
                                      not "D" in install_flag)
            self._info['downgrading'] = "D" in install_flag
            self._info['reinstall'] = "R" in install_flag
            self._package = EmergePackage(r.group(4))
            self._info['REPLACING_VERSIONS'] = r.group(5) or ""
            self._info['SIZE'] = r.group(7) or ""

    def __iter__(self):
        return chain(EmergePackage.attrs, self.attrs)

    def __len__(self):
        if not self['PN']:
            return 0
        else:
            return len(EmergePackage.attrs) + len(self.attrs)

    def __getitem__(self, item):
        if not self._info:
            self._parseData()
        if item in self._info:
            return self._info[item]
        if self._package:
            return self._package[item]
        return None

    def __lt__(self, version):
        if not self._info:
            self._parseData()
        return self._package < version

    def __eq__(self, version):
        if not self._info:
            self._parseData()
        return self._package == version

    def __contains__(self, item):
        if not self._info:
            self._parseData()
        return item in self.attrs or item in self._package

    def __repr__(self):
        return "EmergeUpdateInfo(%s/%s,%s)" % (
            self['CATEGORY'], self['PF'],
            "binary" if self['binary'] else "ebuild")

    def __str__(self):
        return "%s/%s" % (self['CATEGORY'], self['PF'])


@recalculate_update_info
class EmergeRemoveInfo(EmergeUpdateInfo):
    """
    Информация об удалении одного пакета (в списке обновляемых пакетов)
    """
    install_info = "\[(uninstall)([^\]]+)\]"


class Eix(object):
    """
    Вызов eix

    package : пакет или список пакетов
    *options : параметры eix
    all_versions : отобразить все версии пакета или наилучшую
    """
    cmd = getProgPath("/usr/bin/eix")

    class Option(object):
        Installed = '--installed'
        Xml = '--xml'
        Upgrade = '--upgrade'
        TestObsolete = '--test-obsolete'

    default_options = [Option.Xml]

    def __init__(self, package, *options, **kwargs):
        if type(package) in (tuple, list):
            self.package = list(package)
        else:
            self.package = [package]
        self.options = list(options) + self.package + self.default_options
        if not kwargs.get('all_versions', False):
            self.__get_versions = self._get_versions
            self._get_versions = self._get_best_version

    def _get_best_version(self, et):
        ret = None
        for ver in ifilter(lambda x: x.find('mask') is None,
                           et.iterfind('version')):
            ret = ver.attrib['id']
        yield ret

    def _process(self):
        return process(self.cmd, *self.options)

    def get_output(self):
        """
        Получить вывод eix
        """
        with closing(self._process()) as p:
            return p.read()

    def get_packages(self):
        """
        Получить список пакетов
        """
        return list(self._parseXml(self.get_output()))

    def _get_versions(self, et):
        for ver in et.iterfind('version'):
            yield ver.attrib['id']

    def _get_packages(self, et):
        for pkg in et:
            for version in self._get_versions(pkg):
                if version:
                    yield "%s-%s" % (pkg.attrib['name'], version)
                else:
                    yield pkg.attrib['name']

    def _get_categories(self, et):
        for category in et:
            for pkg in self._get_packages(category):
                yield EmergePackage("%s/%s" % (category.attrib['name'], pkg))

    def _parseXml(self, buffer):
        try:
            eix_xml = ET.fromstring(buffer)
            return self._get_categories(eix_xml)
        except ET.ParseError:
            return iter(())


class ChrootEix(Eix):
    """
    Eix выполняемый в chroot
    """

    def __init__(self, chroot_path, package, *options, **kwargs):
        self.chroot_cmd = getProgPath("/usr/bin/chroot")
        self.chroot_path = chroot_path
        self.cmd = getProgPath("/usr/bin/eix", prefix=chroot_path)
        super(ChrootEix, self).__init__(package, *options, **kwargs)

    def _process(self):
        return process(self.chroot_cmd, *([self.chroot_path, self.cmd]
                                          + self.options))


class EmergeLogTask(object):
    def has_marker(self, line):
        """
        Определить есть ли в строке маркер задачи
        """
        return False

    def get_begin_marker(self):
        """
        Получить маркер начала задачи
        """
        return ""

    def get_end_marker(self):
        """
        Получить маркер завершения задачи
        """
        return ""


class EmergeLogNamedTask(EmergeLogTask):
    date_format = "%b %d, %Y %T"

    def __init__(self, taskname):
        self.taskname = taskname

    def has_marker(self, line):
        """
        Определить есть ли в строке маркер задачи
        """
        return self.get_end_marker() in line

    def get_begin_marker(self):
        """
        Получить маркер начала задачи
        """
        return "Calculate: Started {taskname} on: {date}".format(
            taskname=self.taskname,
            date=datetime.datetime.now().strftime(self.date_format))

    def get_end_marker(self):
        """
        Получить маркер завершения задачи
        """
        return " *** Calculate: Finished %s" % self.taskname


class EmergeLogPackageTask(EmergeLogTask):
    def has_marker(self, line):
        """
        Определить есть ли в строке маркер завершения сборки пакета
        """
        return ") Merging " in line


class EmergeLog:
    """
    EmergeLog(after).get_update(package_pattern)
    """
    _emerge_log = "var/log/emerge.log"
    re_complete_emerge = re.compile(r":::\scompleted (emerge) \(.*?\) (\S+)",
                                    re.M)
    re_complete_unmerge = re.compile(r">>>\s(unmerge) success: (\S+)", re.M)

    def __init__(self, emerge_task=EmergeLogTask(), prefix='/'):
        """
        @type emerge_task: EmergeLogTask
        """
        self.emerge_task = emerge_task
        self._list = None
        self._remove_list = None
        self.emerge_log = path.join(prefix, self._emerge_log)

    def _get_last_changes(self):
        """
        Получить список измений по логу, от последней записи маркера
        """
        log_data = SavableIterator(iter(readLinesFile(self.emerge_log)))
        for line in log_data.save():
            if self.emerge_task.has_marker(line):
                log_data.save()
        return log_data.restore()

    def get_last_time(self):
        """
        Получить время послдней записи маркера
        """
        last_line = ""
        for line in readLinesFile(self.emerge_log):
            if self.emerge_task.has_marker(line):
                last_line = line
        return last_line

    @property
    def list(self):
        if self._list is None:
            self.get_packages()
        return self._list

    @property
    def remove_list(self):
        if self._remove_list is None:
            self.get_packages()
        return self._remove_list

    def get_packages(self):
        """
        Получить список пакетов
        """
        self._list, self._remove_list = \
            zip(*self._parse_log(self._get_last_changes()))
        self._list = filter(None, self._list)
        self._remove_list = filter(None, self._remove_list)

    def _parse_log(self, data):
        searcher = lambda x: (self.re_complete_emerge.search(x) or
                              self.re_complete_unmerge.search(x))
        for re_match in ifilter(None, imap(searcher, data)):
            if re_match.group(1) == "emerge":
                yield re_match.group(2), None
            else:
                yield None, re_match.group(2)
        yield None, None

    def _set_marker(self, text_marker):
        with open(self.emerge_log, 'a') as f:
            f.write("{0:.0f}: {1}\n".format(time.time(), text_marker))

    def mark_begin_task(self):
        """
        Отметить в emerge.log начало выполнения задачи
        """
        marker = self.emerge_task.get_begin_marker()
        if marker:
            self._set_marker(marker)

    def mark_end_task(self):
        """
        Отметить в emerge.log завершение выполнения задачи
        """
        marker = self.emerge_task.get_end_marker()
        if marker:
            self._set_marker(marker)


class PackageList(object):
    """
    Список пакетов с возможностью среза и сравнением с версией
    """

    def __init__(self, packages):
        self._raw_list = packages
        self.result = None

    def _packages(self):
        if self.result is None:
            self.result = filter(lambda x: x['PN'],
                                 imap(lambda x: (x if isinstance(x, Mapping)
                                                 else EmergePackage(x)),
                                      self._raw_list))
        return self.result

    def __getitem__(self, item):
        re_item = re.compile(item)
        return PackageList([pkg for pkg in self._packages() if
                            re_item.search(pkg['CATEGORY/PN'])])

    def __iter__(self):
        return iter(self._packages())

    def __len__(self):
        return len(list(self._packages()))

    def __lt__(self, other):
        return any(pkg < other for pkg in self._packages())

    def __le__(self, other):
        return any(pkg <= other for pkg in self._packages())

    def __gt__(self, other):
        return any(pkg > other for pkg in self._packages())

    def __ge__(self, other):
        return any(pkg >= other for pkg in self._packages())

    def __eq__(self, other):
        return any(pkg == other for pkg in self._packages())

    def __ne__(self, other):
        return any(pkg != other for pkg in self._packages())


class Manifest:
    """
    Объект используется для получения данных из Manifest
    файлов портежей
    """
    re_dist = re.compile("^DIST\s*(\S+)\s*")

    def __init__(self, manifest):
        self._manifest = manifest

    def get_dist_files(self):
        """
        Получить список файлов из Manifest
        """
        return imap(lambda x: x.group(1),
                    ifilter(None,
                            imap(self.re_dist.search,
                                 readLinesFile(self._manifest))))


class Ebuild:
    """
    Объект используемый для получения данных из ebuild файлов
    """

    def __init__(self, ebuild):
        self._ebuild = ebuild
        self._category = path.basename(
            path.dirname(
                path.dirname(ebuild)))
        self._pkgname = path.basename(self._ebuild)[:-7]

    def get_tbz2(self):
        """
        Получить имя бинарного пакета с каталогом категории
        """
        return "%s/%s.tbz2" % (self._category, self._pkgname)


def get_packages_files_directory(*directories):
    """
    Получить список бинарных пакетов по ebuild файлам находящихся в директории
    (sys-apps/portage-2.2.0.tbz2)
    """
    for directory in directories:
        directory = path.normpath(directory)
        for ebuild in glob.glob("%s/*/*/*.ebuild" % directory):
            yield Ebuild(ebuild).get_tbz2()


def get_manifest_files_directory(*directories):
    """
    Получить список файлов из всех Manifest находящихся в директориях
    и поддиректориях (portage-2.2.0.tar.bz2)
    """
    for directory in directories:
        directory = path.normpath(directory)
        for manifest in glob.glob("%s/*/*/Manifest" % directory):
            for fn in Manifest(manifest).get_dist_files():
                yield fn


def get_remove_list(directory, filelist, depth=1):
    """
    Получить все файлы из директории, которых нет в filelist
    filelist может содержать как отдельный файл, так и с
    директорией
    """
    directory = path.normpath(directory)
    l = len(directory) + 1
    for fn in ifilter(lambda x: not x[l:] in filelist,
                      find(directory,
                           onefilesystem=True,
                           fullpath=True, depth=depth)):
        if path.isfile(fn):
            yield fn


class SimpleRepositoryMapper(Mapping):
    """
    Определение пути до репозитория
    """
    map_repository = {'gentoo': '/usr/portage',
                      'calculate': '/var/lib/layman/calculate'}
    layman_path = '/var/lib/layman'

    def __init__(self, prefix='/'):
        self.prefix = prefix

    def __getitem__(self, item):
        if item in self.map_repository:
            return pathJoin(self.prefix, self.map_repository[item])
        return pathJoin(self.prefix, self.layman_path, item)

    def __iter__(self):
        return iter(self.map_repository)

    def __len__(self):
        return len(self.map_repository)


class EbuildInfoError(Exception):
    pass


class EbuildInfo(object):
    """
    Информация о ebuild (DEPEND) из metadata
    """
    meta_suffix_path = 'metadata/md5-cache'
    support_keys = ('IUSE', 'DEPEND', 'RDEPEND', 'PDEPEND')

    def __init__(self, atom, rpath):
        meta_path = path.join(rpath,
                              self.meta_suffix_path)
        self._meta_info_path = path.join(meta_path, atom)
        if not path.exists(self._meta_info_path):
            raise EbuildInfoError("Package is not found")
        self._info = self._get_info()

    def _get_info(self):
        with open(self._meta_info_path, 'r') as f:
            return {k.strip(): v.strip() for k, v in (line.partition('=')[::2]
                                                      for line in f.readlines())
                    if k in self.support_keys}

    def __getitem__(self, item):
        if item in self.support_keys:
            return self._info.get(item, '')
        raise KeyError(item)

    def __eq__(self, other):
        return all(other[k] == self[k] for k in self.support_keys)

    def __ne__(self, other):
        res = any(other[k] != self[k] for k in self.support_keys)
        # TODO: DEBUG
        # if res:
        #    for k in self.support_keys:
        #        if other[k] != self[k]:
        #            print k,":", other[k], "!=", self[k]
        return res


class InstalledPackageInfoError(Exception):
    pass


class InstalledPackageInfo(object):
    """
    Информация об установленном пакете (DEPEND) из /var/db/pkg
    """
    depend_pattern = 'declare (?:-x )?({0})="([^"]+)"'
    re_depend = re.compile(depend_pattern.format(
        "|".join(EbuildInfo.support_keys)), re.DOTALL)
    re_multispace = re.compile("\s+", re.DOTALL)

    def __init__(self, atom, pkg_dir):
        self.atom = atom
        self._pkg_path = path.join(pkg_dir, atom)
        if not path.exists(self._pkg_path):
            raise InstalledPackageInfoError("Package is not found")
        self._info = self._get_info()

    def _get_info(self):
        info = {k: "" for k in EbuildInfo.support_keys}
        env_path = path.join(self._pkg_path, 'environment.bz2')
        if path.exists(env_path):
            with bz2.BZ2File(env_path, 'r') as f:
                for r in self.re_depend.finditer(f.read()):
                    key, value = r.groups()
                    value = self.re_multispace.sub(" ", value)
                    info[key] = value.strip()
        rep_path = path.join(self._pkg_path, 'repository')
        info['repository'] = readFile(rep_path).strip()
        return info

    def __getitem__(self, item):
        return self._info[item]

    @classmethod
    def get_install_packages(cls, pkg_dir='/var/db/pkg'):
        for category in listDirectory(pkg_dir):
            for pkg in listDirectory(path.join(pkg_dir, category)):
                if "MERGING" not in pkg:
                    yield InstalledPackageInfo("%s/%s" % (category, pkg),
                                               pkg_dir=pkg_dir)

    def __str__(self):
        return self.atom

    def __repr__(self):
        return "InstalledPackageInfo(%s)" % self.atom
