#!/usr/bin/python
# Copyright (C) 2018-2020 Jelmer Vernooij <jelmer@jelmer.uk>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

from typing import Optional, Callable, List, Union, Dict, BinaryIO, Any, Tuple

from breezy.branch import Branch
from breezy.tree import Tree
from breezy.workingtree import WorkingTree
from breezy.diff import show_diff_trees
from breezy.errors import (
    DivergedBranches,
    NotBranchError,
    NoColocatedBranchSupport,
    )
from breezy.propose import (
    get_hoster,
    Hoster,
    MergeProposal,
    )

from breezy.trace import note

from .publish import (
    merge_conflicts,
    propose_changes,
    push_changes,
    push_derived_changes,
    publish_changes as _publish_changes,
    PublishResult,
    )
from .utils import (
    create_temp_sprout,
    full_branch_url,
    )


__all__ = [
    'Workspace',
    ]


class Workspace(object):
    """Workspace for creating changes to a branch.

    main_branch: The upstream branch
    resume_branch: Optional in-progress branch that we previously made changes
        on, and should ideally continue from.
    cached_branch: Branch to copy revisions from, if possible.
    local_tree: The tree the user can work in
    """

    _destroy: Optional[Callable[[], None]]
    local_tree: WorkingTree
    main_branch_revid: Optional[bytes]

    def __init__(self, main_branch: Branch,
                 resume_branch: Optional[Branch] = None,
                 cached_branch: Optional[Branch] = None,
                 additional_colocated_branches: Optional[List[str]] = None,
                 dir: Optional[str] = None,
                 path: Optional[str] = None) -> None:
        self.main_branch = main_branch
        self.main_branch_revid = None
        self.cached_branch = cached_branch
        self.resume_branch = resume_branch
        self.additional_colocated_branches = (
            additional_colocated_branches or [])
        self._destroy = None
        self._dir = dir
        self._path = path

    def __str__(self):
        if self._path is None:
            return "Workspace for %s" % full_branch_url(self.main_branch)
        else:
            return "Workspace for %s at %s" % (
                full_branch_url(self.main_branch), self._path)

    def __repr__(self):
        return (
            "%s(%r, resume_branch=%r, cached_branch=%r, "
            "additional_colocated_branches=%r, dir=%r, path=%r)" % (
                type(self).__name__, self.main_branch, self.resume_branch,
                self.cached_branch, self.additional_colocated_branches,
                self._dir, self._path))

    def __enter__(self) -> Any:
        self.local_tree, self._destroy = create_temp_sprout(
            self.cached_branch or self.resume_branch or self.main_branch,
            self.additional_colocated_branches,
            dir=self._dir, path=self._path)
        self.main_branch_revid = self.main_branch.last_revision()
        self.refreshed = False
        with self.local_tree.branch.lock_write():
            if self.cached_branch:
                self.local_tree.pull(
                    self.resume_branch or self.main_branch, overwrite=True)
            if self.resume_branch:
                try:
                    self.local_tree.pull(self.main_branch, overwrite=False)
                except DivergedBranches:
                    pass
                for branch_name in self.additional_colocated_branches:
                    try:
                        remote_colo_branch = (
                            self.main_branch.controldir.open_branch(
                                name=branch_name))
                    except (NotBranchError,
                            NoColocatedBranchSupport):
                        continue
                    self.local_tree.branch.controldir.push_branch(
                        name=branch_name, source=remote_colo_branch,
                        overwrite=True)
                if merge_conflicts(self.main_branch, self.local_tree.branch):
                    note('restarting branch')
                    self.local_tree.update(revision=self.main_branch_revid)
                    self.local_tree.branch.generate_revision_history(
                        self.main_branch_revid)
                    self.resume_branch = None
                    self.refreshed = True
            self.orig_revid = self.local_tree.last_revision()
        return self

    def defer_destroy(self) -> Optional[Callable[[], None]]:
        ret = self._destroy
        self._destroy = None
        return ret

    def changes_since_main(self) -> bool:
        return self.local_tree.branch.last_revision() != self.main_branch_revid

    def changes_since_resume(self) -> bool:
        return self.orig_revid != self.local_tree.branch.last_revision()

    def push(self, hoster: Optional[Hoster] = None, dry_run: bool = False,
             tags: Optional[Union[Dict[str, bytes], List[str]]] = None,
             stop_revision: Optional[bytes] = None) -> None:
        if hoster is None:
            hoster = get_hoster(self.main_branch)
        return push_changes(
            self.local_tree.branch, self.main_branch, hoster=hoster,
            additional_colocated_branches=self.additional_colocated_branches,
            dry_run=dry_run, tags=tags, stop_revision=stop_revision)

    def propose(self, name: str, description: str,
                hoster: Optional[Hoster] = None,
                existing_proposal: Optional[MergeProposal] = None,
                overwrite_existing: Optional[bool] = None,
                labels: Optional[List[str]] = None,
                dry_run: bool = False,
                commit_message: Optional[str] = None,
                reviewers: Optional[List[str]] = None,
                tags: Optional[Union[Dict[str, bytes], List[str]]] = None,
                owner: Optional[str] = None,
                allow_collaboration: bool = False,
                stop_revision: Optional[bytes] = None) -> MergeProposal:
        if hoster is None:
            hoster = get_hoster(self.main_branch)
        return propose_changes(
            self.local_tree.branch, self.main_branch, hoster=hoster, name=name,
            mp_description=description, resume_branch=self.resume_branch,
            resume_proposal=existing_proposal,
            overwrite_existing=(overwrite_existing or False), labels=labels,
            dry_run=dry_run, commit_message=commit_message,
            reviewers=reviewers, owner=owner,
            additional_colocated_branches=self.additional_colocated_branches,
            tags=tags, allow_collaboration=allow_collaboration,
            stop_revision=stop_revision)

    def push_derived(self,
                     name: str, hoster: Optional[Hoster] = None,
                     overwrite_existing: Optional[bool] = False,
                     owner: Optional[str] = None,
                     tags: Optional[Union[Dict[str, bytes], List[str]]] = None,
                     stop_revision: Optional[bytes] = None
                     ) -> Tuple[Branch, str]:
        """Push a derived branch.

        Args:
          name: Branch name
          hoster: Optional hoster to use
          overwrite_existing: Whether to overwrite an existing branch
          tags: Tags list to push
          owner: Owner name
        Returns:
          tuple with remote_branch and public_branch_url
        """
        if hoster is None:
            hoster = get_hoster(self.main_branch)
        return push_derived_changes(
            self.local_tree.branch,
            self.main_branch, hoster, name,
            overwrite_existing=overwrite_existing,
            owner=owner, tags=tags, stop_revision=stop_revision)

    def publish_changes(self, *args, **kwargs) -> PublishResult:
        """Publish a set of changes.
        """
        return _publish_changes(
            self.local_tree.branch, self.main_branch, self.resume_branch,
            *args, **kwargs)

    def orig_tree(self) -> Tree:
        return self.local_tree.branch.repository.revision_tree(self.orig_revid)

    def show_diff(self, outf: BinaryIO,
                  old_label: str = 'old/', new_label: str = 'new/') -> None:
        orig_tree = self.orig_tree()
        show_diff_trees(
            orig_tree, self.local_tree.basis_tree(), outf,
            old_label=old_label, new_label=new_label)

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self._destroy:
            self._destroy()
            self._destroy = None
        return False
