#-*- 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.

import os
import re
import sys
import time
from os import path
from subprocess import Popen
from calculate.lib.utils.files import (process,getProgPath,STDOUT,removeDir,
            processProgress,PercentProgress,process,readFile)

import xml.etree.ElementTree as ET


from calculate.lib.cl_lang import setLocalTranslate,getLazyLocalTranslate
setLocalTranslate('cl_update3',sys.modules[__name__])
__ = getLazyLocalTranslate(_)

class AddonError(Exception):
    """
    Исключение с добавочным сообщением
    """
    def __init__(self, msg, addon=None):
        self.message = msg
        self.addon = addon
        Exception.__init__(self,msg)

class UpdateError(AddonError):
    """Update Error"""

class GitError(AddonError):
    """Git Error"""

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

    Args:
        installed: путь до installed.xml
        makeconf: путь до makeconf
    """
    def __init__(self,installed,makeconf):
        self.installed = installed
        self.makeconf = makeconf

    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
            indent(root)
        except ImportError as e:
            pass
        with open(self.installed,'w') as f:
            f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
            tree.write(f, encoding="utf-8")

    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 'PORTDIR_OVERLAY="\n%s"'%"\n".join(repos);
        if path.exists(self.makeconf):
            content = readFile(self.makeconf)
            if "PORTDIR_OVERLAY" in content:
                new_content = re.sub("\APORTDIR_OVERLAY=\"([^\"]+)\"",
                                 fixContent, content,re.DOTALL)
                if new_content == content:
                    return
                else:
                    content = new_content
            else:
                content = 'PORTDIR_OVERLAY="\n%s"\n'%rpath + content
        else:
            content = 'PORTDIR_OVERLAY="\n%s"\n'%rpath
        with open(self.makeconf,'w') as f:
            f.write(content)

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

class Git:
    """
    Объект для управление git репозиторием
    """
    def __init__(self):
        self._git = self.__getGit()

    def _checkExistsRep(self,rpath):
        """
        Проверить путь на наличие репозитория
        """
        if path.exists(rpath):
            if not path.isdir(rpath):
                raise GitError(
                    _("Repository {path} is not directory").format(
                    path=rpath))
            if not path.isdir(self.__gitDir(rpath)):
                raise GitError(
                    _("Repository {path} is not git").format(
                    path=rpath))
            return True
        return False

    def __getGit(self):
        """
        Получить утилиту git
        """
        git = getProgPath("/usr/bin/git")
        if not git:
            raise GitError(_("Git utility is not found"))
        return git

    def __gitDir(self,rpath):
        return path.join(rpath,".git")

    def _cloneRepository(self, url, rpath, branch, cb_progress=None):
        """
        Сделать локальную копию репозитория

        Args:
            url: откуда качать репозиторий
            rpath: куда сохранять репозиторий
            branch: ветка на которую необходимо переключиться
        """
        if cb_progress:
            gitClone = PercentProgress(self._git,"clone","-q",
                       "--no-single-branch","--progress","--verbose",
                       "--depth=1","-b",branch,url,rpath,part=4,stderr=STDOUT)
            for perc in gitClone.progress():
                cb_progress(perc)
        else:
            gitClone = process(self._git,"clone","-q","--no-single-branch",
                       "--depth=1","-b",branch,url,rpath,stderr=STDOUT)
        if gitClone.failed():
            error = gitClone.read()
            if "Remote branch %s not found"%branch in error:
                raise GitError(
                    _("Branch {branch} not found in {url} repository").format(
                    branch=branch,url=url))
            raise GitError(_("Failed to clone {url} repository").format(
                url=url),error)
        return True

    def _cloneRevRepository(self, url, rpath, branch,revision,cb_progress=None):
        """
        Сделать локальную копию репозитория с указанной ревизией

        Args:
            url: откуда качать репозиторий
            rpath: куда сохранять репозиторий
            branch: ветка на которую необходимо переключиться
            revision: если указана - сделать ревизию текущей

        Return:
            Возвращает True если клонирование произведено с установкой на
            указанную ревизию. False если клонирование произведено с
            установкой на последнюю ревизию.
        Raises:
            GitError: Выполнение ключевых команд выполнено с ошибками (не
                удалось скачать и получить данные из удаленного репозитория)
        """
        git_dir = self.__gitDir(rpath)
        error = []

        def command(cmd,startpart=0):
            """
            Выполнить одну из команд необходимой для клонирования репозитория
            """
            commands = {# инициализация пустого репозитория 
                        'init': ["init",rpath],
                        # подключить указанный удаленный как origin
                        'add_remote':["remote","add","origin",url],
                        # скачать последние коммиты веток
                        'fetchshallow': ["fetch","--depth=1"],
                        # проверить есть указанный коммит
                        'has_revision': ["log","-n1",revision],
                        # проверить есть указанный коммит
                        'has_branch': ["log","-n1","remotes/origin/%s"%branch],
                        # получить ревизию из revs тэгов
                        'get_rev_tag': ["fetch","--depth=1","origin",
                                "+refs/revs/%s:refs/remotes/origin/%s"%
                                    (revision,branch)],
                        # переключиться на указанную ревизию указанной веткой
                        'checkout_revision': ["checkout","-b",branch,revision],
                        # переключить на указанную ветку
                        'checkout': ["checkout",branch],
                        # установить upstream для локальной ветки
                        'set_track': ["branch",branch,'-u',"origin/%s"%branch]
                       }
            if cmd == "init":
                wholeCommand = [self._git]+commands[cmd]
            else:
                wholeCommand = [self._git,"--git-dir",git_dir,
                                 "--work-tree",rpath]+commands[cmd]
            if cb_progress and commands[cmd][0] in ("fetch","checkout"):
                progressParam = {'fetch':   {'part':4,'end':False},
                                 'checkout':{'part':4,'startpart':3}}
                gitClone = PercentProgress(*wholeCommand+["--progress","--verbose"],
                                           stderr=STDOUT,**progressParam)
                for perc in gitClone.progress():
                    cb_progress(perc)
            else:
                gitCmd = process(*wholeCommand,stderr=STDOUT)
            if gitCmd.failed():
                error.append(gitCmd.read())
                return False
            return True

        # получить последние коммиты из удаленного репозитория
        if command("init") and command("add_remote"):
            if command("get_rev_tag") or command("fetchshallow"):
                if not command("has_branch"):
                    raise GitError(
                        _("Branch {branch} not found in {url} repository"
                         ).format(branch=branch,url=url))
                # если среди коммитов есть указанный коммит
                if command("has_revision"):
                    # переключаемся на нужный коммита, устанавливаем связь
                    if command("checkout_revision") and command("set_track"):
                        return True
                elif command("checkout"):
                    return False
        raise GitError(_("Failed to clone {url} repository").format(
            url=url),error[-1])

    def _pullRepository(self,rpath,quiet_error=False,cb_progress=None):
        """
        Обновить репозиторий до последней версии
        """
        gitPull = process(self._git,"--git-dir",self.__gitDir(rpath),
                          "pull","--ff-only",stderr=STDOUT)
        if gitPull.failed():
            if not quiet_error:
                error = gitPull.read()
                raise GitError(
                    _("Failed to update repository in {rpath}").format(
                    rpath=rpath),error)
            return False
        return True

    def _fetchRepository(self, rpath, cb_progress=None):
        """
        Получить изменения из удаленно репозитория
        """
        if cb_progress:
            gitFetch = PercentProgress(self._git,"--git-dir",self.__gitDir(rpath),
                          "fetch","--progress","--verbose",part=3,stderr=STDOUT)
            for perc in gitFetch.progress():
                cb_progress(perc)
        else:
            gitFetch = process(self._git,"--git-dir",self.__gitDir(rpath),
                          "fetch",stderr=STDOUT)
        if gitFetch.failed():
            error = gitFetch.read()
            raise GitError(
                _("Failed to update repository in {rpath}").format(
                rpath=rpath),error)
        return True

    def _checkChanges(self,rpath):
        """
        Проверить наличие изменений пользователем файлов в репозитории
        """
        git_dir = self.__gitDir(rpath)
        git_status = process(self._git,"--git-dir",git_dir,"--work-tree",rpath,
                            "status","--porcelain",stderr=STDOUT)
        if git_status.success():
            for i in git_status:
                if i.strip():
                    return False
            else:
                return True
        else:
            raise GitError(
                _("Wrong repository in {rpath} directory").format(
                rpath=rpath))

    def _parseStatusInfo(self,data):
        """
        Разобрать информацию полученную через git status -b --porcelain

        Returns:
            Словарь
            # есть ли измененные файлы пользователем
            {'files':True/False,
            # есть коммиты после текущего
             'ahead':True/False,
            # есть коммиты перед текущим (означает, что обновление
            # с основной веткой не осуществляется через fast-forward
             'behind':True/False,
            # текущая ветка
             'branch':'',
            # оригинальная ветка
             'origin':'origin/master'}
        """
        reStatus = re.compile("^## (\w+)(?:\.\.\.(\S+)\s+\[(ahead \d+)?"
                              "(?:, )?(behind \d+)?\])?\n?(.*|$)",re.S)
        match = reStatus.search(data)
        if not match:
            return {}
        return {'files':True if match.group(5) else False,
                'ahead':True if match.group(3) else False,
                'behind':True if match.group(4) else False,
                'origin':match.group(2) or "",
                'branch':match.group(1)}

    def _getCurrentCommit(self,rpath):
        """
        Получить текущий коммит в репозитории
        """
        git_dir = self.__gitDir(rpath)
        git_show = process(self._git,"--git-dir",git_dir,"show","--format=format:%H",
                           "--quiet",stderr=STDOUT)
        if git_show.success():
            return git_show.read().strip()
        else:
            raise GitError(
                _("Failed to get status of repository in "
                  "{rpath} directory").format(
                  rpath=rpath))

    def _getStatusInfo(self,rpath):
        """
        Получить информацию об изменениях в репозитории

        Returns:
            Словарь выдаваемый функцией _parseStatusInfo
        """
        git_dir = self.__gitDir(rpath)
        git_status = process(self._git,"--git-dir",git_dir,"--work-tree",rpath,
                            "status","-b","--porcelain",stderr=STDOUT)
        if git_status.success():
            retDict = self._parseStatusInfo(git_status.read())
            if not retDict:
                raise GitError(
                    _("Failed to get status of repository in "
                      "{rpath} directory").format(
                      rpath=rpath))
            return retDict
        else:
            raise GitError(
                _("Wrong repository in {rpath} directory").format(
                rpath=rpath))

    def _resetRepository(self, rpath, to_origin=False, to_rev=None, info=None):
        """
        Удалить неиндексированные изменения в репозитории


        Args:
            to_origin: откатить все изменения до удаленного репозитория
            to_rev: откатить все изменения до определенной ревизии
            info: использовать уже полученную информация об изменения в репозитории
        Return:
            True - успешное выполнение
            False - нет необходимости выполнять reset
        Raises:
            GitError: выполнение комманд reset и clean прошло с ошибкой
        """
        git_dir = self.__gitDir(rpath)
        if not info:
            info = self._getStatusInfo(rpath)
        if (all(not info[x] for x in ("files","ahead","behind") if x in info) 
            and (not info["origin"] or 
                 "origin/%s"%info["branch"] == info["origin"])):
            return False
        commit = (info['origin'] if to_origin else to_rev) or "HEAD"
        git_reset = process(self._git,"--git-dir",git_dir,"--work-tree",rpath,
                                "reset","--hard", commit, stderr=STDOUT)
        git_clean = process(self._git,"--git-dir",git_dir,"--work-tree",rpath,
                            "clean","-fd",stderr=STDOUT)
        if git_reset.failed() or git_clean.failed():
            raise GitError(_("Failed to clean {rpath} repository").format(
                    rpath=rpath))
        return True

    def _getBranch(self,rpath):
        """
        Получить текущую ветку
        """
        return self._getStatusInfo(rpath)['branch']

    def _checkoutBranch(self,rpath,branch):
        """
        Сменить ветку
        """
        git_dir = self.__gitDir(rpath)
        git_checkout = process(self._git,"--git-dir",git_dir,
                        "--work-tree", rpath,
                        "checkout","-f",branch, stderr=STDOUT)
        if git_checkout.failed():
            error = git_checkout.read()
            if "pathspec '%s' did not match"%branch in error:
                raise GitError(
                    _("Branch {branch} not found in {rpath} repository").format(
                    branch=branch,rpath=rpath))
            raise GitError(
                _("Failed to change branch to {branch} in "
                  "{rpath} repository").format(branch=branch,
                    rpath=rpath),error)
        return True

class Update:
    """Основной объект для выполнения действий связанных с обновлением системы

    """

    def _syncRepository(self,name,url,rpath,revision,branch,
                        cb_progress=None):
        """
        Синхронизировать репозиторий
        """
        dv = self.clVars
        git = Git()
        needMeta = False
        if not git._checkExistsRep(rpath):
            if revision == "last":
                git._cloneRepository(url, rpath, branch,
                    cb_progress=cb_progress)
            else:
                git._cloneRevRepository(url, rpath, branch, revision,
                    cb_progress=cb_progress)
            needMeta = True
        else:
            # если нужно обновиться до конкретной ревизии
            if revision != "last":
                if revision == git._getCurrentCommit(rpath):
                    if git._getBranch(rpath) == branch:
                        return True
            # получить изменения из удаленного репозитория
            git._fetchRepository(rpath,cb_progress=cb_progress)
            # если текущая ветка не соответствует нужной
            repInfo = git._getStatusInfo(rpath)
            if repInfo['branch'] != branch:
                # меняем ветку
                needMeta = True
                git._checkoutBranch(rpath,branch)
            if revision == "last":
                if git._resetRepository(rpath, to_origin=True):
                    needMeta = True
            else:
                git._resetRepository(rpath, to_rev=revision)
                needMeta = True
        if needMeta:
            dv.Set('cl_update_outdate_set','on',force=True)
        layman = Layman(dv.Get('cl_update_layman_installed'),
                        dv.Get('cl_update_layman_make'))
        if name != "portage":
            layman.add(name,url,rpath)
        return True

    def syncRepositories(self,repname,clean_on_error=True):
        """
        Синхронизировать репозитории
        """
        dv = self.clVars
        url, rpath, revision, branch = (
            dv.Select(["cl_update_rep_url","cl_update_rep_path",
                       "cl_update_rep_rev","cl_update_branch_name"],
                       where="cl_update_rep_name",eq=repname,limit=1))
        if not url or not rpath:
            raise UpdateError(_("Repositories variables is not configured"))
        self.addProgress()
        if clean_on_error:
            try:
                self._syncRepository(repname,url,rpath,revision,branch,
                    cb_progress=self.setProgress)
                return True
            except GitError as e:
                if e.addon:
                    self.printWARNING(str(e.addon))
                self.printWARNING(str(e))
                self.printWARNING(_("Re-fetch {name} repository"
                                  ).format(name=repname))
                removeDir(rpath)
        self._syncRepository(name,url,rpath,revision,branch)
        return True

    def syncLaymanRepository(self,repname):
        """
        Обновить репозиторий через layman
        """
        layman = getProgPath('/usr/bin/layman')
        if not layman:
            raise UpdateError(_("Layman utility is not found"))
        rpath = self.clVars.Select('cl_update_other_rep_path',
            where='cl_update_other_rep_name',eq=repname,limit=1)
        laymanname = path.basename(rpath)
        if path.exists(path.join(rpath,'.git')):
            self.addProgress()
            p = PercentProgress(layman,"-s",laymanname,part=1,atty=True)
            for perc in p.progress():
                self.setProgress(perc)
        else:
            p = process(layman,"-s",repname,stderr=STDOUT)
        if p.failed():
            raise UpdateError(_("Failed to update repository {rname}"
                    ).format(rname=repname),addon=p.read())
        return True

    def regenCache(self,repname):
        """
        Обновить кэш метаданных репозитория
        """
        egenCache = getProgPath('/usr/bin/egencache')
        if not egenCache:
            raise UpdateError(_("Portage utility is not found"))
        cpu_num = self.clVars.Get('hr_cpu_num')
        p = process(egenCache,"--repo=%s"%repname,"--update",
                    "--jobs=%s"%cpu_num,stderr=STDOUT)
        if p.failed():
            raise UpdateError(_("Failed to update cache of {rname} repository"
                    ).format(rname=repname),addon=p.read())
        return True

    def emergeMetadata(self):
        """
        Выполнить egencache и emerge --metadata
        """
        emerge = getProgPath("/usr/bin/emerge")
        if not emerge:
            raise UpdateError(_("Emerge utility is not found"))
        self.addProgress()
        p = PercentProgress(emerge,"--metadata",part=1,atty=True)
        for perc in p.progress():
            self.setProgress(perc)
        if p.failed():
            raise UpdateError(_("Failed to update metadata"),addon=p.read())
        return True

    def eixUpdate(self):
        """
        Выполенине eix-update для репозиторием

        eix-update выполнятется только для тех репозиториев, которые
        обновлялись, если cl_update_eixsync_force==auto, либо
        все, если cl_update_eixupdate_force==force
        """
        eixupdate = getProgPath("/usr/bin/eix-update")
        if not eixupdate:
            raise UpdateError(_("Eix utility is not found"))
        self.addProgress()
        excludeList = []
        if self.clVars.Get('cl_update_eixupdate_force') == 'force':
            countRep = len(self.clVars.Get('cl_update_rep_name'))
        else:
            for rep in self.clVars.Get('cl_update_rep_name'):
                # подстановка имен
                mapNames = {'portage':'gentoo'}
                if not rep in self.clVars.Get('cl_update_sync_rep'):
                    excludeList.extend(["-x",mapNames.get(rep,rep)])
            countRep = len(self.clVars.Get('cl_update_sync_rep'))
        if (self.clVars.Get('cl_update_other_set') == 'on' or 
            self.clVars.Get('cl_update_eixupdate_force') == 'force'):
            countRep += len(self.clVars.Get('update.cl_update_other_rep_name'))
        else:
            for rep in self.clVars.Get('update.cl_update_other_rep_name'):
                excludeList.extend(['-x',rep])
        p = PercentProgress(eixupdate,"-F",*excludeList,part=countRep or 1,atty=True)
        for perc in p.progress():
            self.setProgress(perc)
        if p.failed():
            raise UpdateError(_("Failed to update eix cache"),addon=p.read())
        return True
