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

# Copyright 2015 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 re
import sys
from os import path
from calculate.lib.configparser import ConfigParser

from calculate.lib.utils.files import (getProgPath, STDOUT,
                                       PercentProgress, process, readFile,
                                       writeFile, listDirectory)
from calculate.lib.utils.tools import AddonError

_ = lambda x: x
from calculate.lib.cl_lang import setLocalTranslate

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

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

class Git:
    """
    Объект для управление git репозиторием
    """
    skip_files = ["metadata/md5-cache",
                  "metadata/cache"]

    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 a 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(_("The Git tool is not found"))
        return git

    @staticmethod
    def _gitDir(rpath):
        return path.join(rpath, ".git")

    @staticmethod
    def is_git(gitpath):
        return path.isdir(Git._gitDir(gitpath))

    def get_url(self, rpath, remote_name):
        config = ConfigParser(strict=False)
        config.read(path.join(Git._gitDir(rpath), "config"), encoding="utf-8")
        return config.get('remote "%s"'%remote_name, "url",
                          raw=True, fallback="")

    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 repository {url}").format(
                        branch=branch, url=url))
            raise GitError(_("Failed to clone repository {url}").format(
                url=url), error)
        return True

    def _git_command(self, git_dir, rpath, error, cb_progress=None):
        def command(*args, **kw):
            if "init" == args[0]:
                wholeCommand = [self._git, "init", rpath] + list(args[1:])
            else:
                wholeCommand = [self._git, "--git-dir", git_dir,
                                "--work-tree", rpath] + list(args)
            if cb_progress and args[0] in ("reset", "fetch", "checkout"):
                gitCmd = PercentProgress(
                    *wholeCommand,
                    stderr=STDOUT, atty=True, **kw)
                for perc in gitCmd.progress():
                    cb_progress(perc)
            else:
                gitCmd = process(*wholeCommand, stderr=STDOUT)
            if gitCmd.failed():
                error.append(gitCmd.read())
                return False
            return True
        return command

    def _getFetchHeadInfo(self, git_dir):
        data = readFile(path.join(git_dir,"FETCH_HEAD")).strip()
        re_fetch = re.compile(r"^(\w+)\s+(?:(branch|tag) '([^']+)' of )?(\S+)$")
        m = re_fetch.search(data)
        if m:
            d = {'commit':m.group(1),
                 'url':m.group(4)}
            if m.group(2):
                type_map = {'tag': self.Reference.Tag,
                            'branch': self.Reference.Branch}
                d['type'] = type_map.get(m.group(2), self.Reference.Unknown)
                d['reference'] = m.group(3)
            return d
        return {}


    def cloneTagRepository(self, url, rpath, reference, cb_progress=None):
        """
        Сделать локальную копию репозитория для переключения по тэгам

        в отличии от обычного репозитория не содерит не remote, не ветки,
        только срез ревизии и тэги

        :param url:  откуда качать репозиторий
        :param rpath: куда сохранять репозиторий
        :param reference: метка на которую нужно перейти
        :param cb_progress: callback для прогрессбара
        :return: True в случае успешного клонирования на указанную метку
        :raise: GitError не удалось скачать, получить данные, метка не найдена
        """
        git_dir = self._gitDir(rpath)
        error = []

        command = self._git_command(git_dir, rpath, error, cb_progress)
        # получить последние коммиты из удаленного репозитория
        if command("init"):
            if not command("fetch", "--depth=1", url, reference, part=4,
                           end=False):
                raise GitError(
                    _("Reference {mark} not found in repository {url}"
                      ).format(mark=reference, url=url))
            if command("reset", "--hard", "FETCH_HEAD", part=4, startpart=3):
                if self.saveFetchHead(git_dir) == self.Reference.Branch:
                    command("symbolic-ref", "HEAD", "refs/heads/%s" % reference)
                return True
        raise GitError(_("Failed to clone repository {url}").format(
            url=url), error[-1])
        #progressParam = {'fetch': {'part': 4, 'end': False},
        #                 'checkout': {'part': 4, 'startpart': 3}}

    def isTagRepository(self, rpath):
        """
        :param rpath: путь до репозитория
        :return: True репозиторий переключается по тэгам
        """
        git_path = self._gitDir(rpath)
        data = self._getFetchHeadInfo(git_path)
        if "type" in data and data['type'] == self.Reference.Tag:
            return True
        return False

    class Reference:
        Branch = "branch"
        Tag = "tag"
        #RemoteBranch = "remote_branch"
        Unknown = "unknown"
        Master = "master"
        Develop = "develop"
        Update = "update"

    def reference_type(self, rpath, reference):
        """
        Получить тип ссылки на коммит
        :param rpath: путь до локального репозитория
        :param reference: имя ссылки
        :return: Reference (Branch - ветка, Tag - тэг,
                                Unknown - всё остальное)
        """
        refs_dir = path.join(rpath, ".git/refs")
        if reference in listDirectory(path.join(refs_dir, 'tags')):
            return self.Reference.Tag
        elif reference in listDirectory(path.join(refs_dir, 'heads')):
            return self.Reference.Branch
        return self.Reference.Unknown


    def updateTagRepository(self, url, rpath, reference, cb_progress=None):
        """
        Обновить данные до указанного тега репозитория

        :param url:  откуда качать репозиторий
        :param rpath: куда сохранять репозиторий
        :param reference: метка на которую нужно перейти
        :param cb_progress: callback для прогрессбара
        :return: True в случае успешного клонирования на указанную метку
        :raise: GitError не удалось скачать, получить данные, метка не найдена
        """
        git_dir = self._gitDir(rpath)
        error = []

        command = self._git_command(git_dir, rpath, error, cb_progress)
        # получить последние коммиты из удаленного репозитория
        try:
            if (self.reference_type(rpath, reference) == Git.Reference.Tag and
                self.isTagRepository(rpath) and
                self.getCurrentCommit(rpath) == self.getCommit(rpath,
                                                               reference)):
                return False
        except GitError:
            pass
        if not command("fetch", "--depth=1", url, reference, part=4, end=False):
            raise GitError(
                _("Reference {mark} not found in repository {url}"
                  ).format(mark=reference, url=url))
        if command("reset", "--hard", "FETCH_HEAD", part=4, startpart=3):
            command("clean", "-df")
            if self.saveFetchHead(git_dir) == self.Reference.Branch:
                command("symbolic-ref", "HEAD", "refs/heads/%s" % reference)
            return True
        raise GitError(_("Failed to clone repository {url}").format(
            url=url), error[-1])

    # TODO: метод удаление лишних объектов, оставляя только текущий коммит

    def saveFetchHead(self, git_dir):
        """
        Сохранить FEACH_HEAD как объект (tag или branch)
        :param git_dir: каталог .git репозитория
        :return: Reference
        """
        fetch_info = self._getFetchHeadInfo(git_dir)
        tag_file = None
        if fetch_info.get('type', '') == self.Reference.Tag:
            tag_file = path.join(git_dir, "refs/tags/%s"
                                % fetch_info['reference'])
        elif fetch_info.get('type', '') == self.Reference.Branch:
            tag_file = path.join(git_dir, "refs/heads/%s"
                                 % fetch_info['reference'])
        if tag_file:
            with writeFile(tag_file) as f:
                f.write(fetch_info['commit'])
            return fetch_info['type']
        return self.Reference.Unknown

    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/tags/%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 repository {url}"
                        ).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 repository {url}").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 the 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 the 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():
            return not any(x.strip() for x in git_status)
        else:
            raise GitError(
                _("Wrong repository in the {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*(\(no branch\))?(?:\.\.\.(\S+))?(?:\s+\[(ahead \d+)?"
                              "(?:, )?(behind \d+)?\])?\n?(.*|$)", re.S)
        match = reStatus.search(data)
        if not match:
            return {}
        return {'files': True if match.group(6) else False,
                'ahead': True if match.group(4) else False,
                'behind': True if match.group(5) else False,
                'origin': match.group(3) or "",
                'branch': "" if match.group(2) else match.group(1)}

    def getCurrentCommit(self, rpath):
        """
        Получить текущий коммит в репозитории
        """
        return self.getCommit(rpath, "HEAD")

    def getCommit(self, rpath, reference):
        git_dir = self._gitDir(rpath)
        git_show = process(self._git, "--git-dir", git_dir, "show",
                           "--format=format:%H",
                           "--quiet", reference, stderr=STDOUT)
        if git_show.success():
            return git_show.read().strip()
        else:
            raise GitError(
                _("Failed to get the repository status for {rpath}").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 the repository status for {rpath}").format(
                        rpath=rpath))
            return retDict
        else:
            raise GitError(
                _("Wrong repository in the {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 the {rpath} repository").format(
                rpath=rpath))
        return True

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

    def getTags(self, rpath):
        """
        Получить список тэгов, отсортированных по версии
        """
        git_dir = self._gitDir(rpath)
        git_tag = process(self._git, "--git-dir", git_dir, "--work-tree",
                          rpath,
                          "tag", "--sort=version:refname", stderr=STDOUT)
        if git_tag.success():
            return [x.strip() for x in git_tag if x.strip()]
        return []

    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 repository {rpath}").format(
                        branch=branch, rpath=rpath))
            raise GitError(
                _("Failed to move to branch {branch} in the {rpath} "
                  "repository").format(branch=branch,
                                               rpath=rpath), error)
        return True
