#  Copyright (C) 2017 Codethink Limited
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2 of the License, or (at your option) any later version.
#
#  This library 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
#  Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with this library. If not, see <http://www.gnu.org/licenses/>.
#
#  Authors:
#        Jonathan Maw <jonathan.maw@codethink.co.uk>

"""
bzr - stage files from a bazaar repository
==========================================

**Host dependencies:**

  * bzr

**Usage:**

.. code:: yaml

   # Specify the bzr source kind
   kind: bzr

   # Optionally specify a relative staging directory
   # directory: path/to/stage

   # Specify the bzr url. Bazaar URLs come in many forms, see
   # `bzr help urlspec` for more information. Using an alias defined
   # in your project configuration is encouraged.
   url: https://launchpad.net/bzr

   # Specify the tracking branch. This is mandatory, as bzr cannot identify
   # an individual revision outside its branch. bzr URLs that omit the branch
   # name implicitly specify the trunk branch, but bst requires this to be
   # explicit.
   track: trunk

   # Specify the ref. This is a revision number. This is usually a decimal,
   # but revisions on a branch are of the form
   # <revision-branched-from>.<branch-number>.<revision-since-branching>
   # e.g. 6622.1.6.
   # The ref must be specified to build, and 'bst track' will update the
   # revision number to the one on the tip of the branch specified in 'track'.
   ref: 6622

"""

import os
import shutil
import fcntl
from contextlib import contextmanager

from buildstream import Source, SourceError, Consistency
from buildstream import utils


class BzrSource(Source):
    # pylint: disable=attribute-defined-outside-init

    def configure(self, node):
        self.node_validate(node, ['url', 'track', 'ref'] + Source.COMMON_CONFIG_KEYS)

        self.original_url = self.node_get_member(node, str, 'url')
        self.tracking = self.node_get_member(node, str, 'track')
        self.ref = self.node_get_member(node, str, 'ref', None)
        self.url = self.translate_url(self.original_url)

    def preflight(self):
        # Check if bzr is installed, get the binary at the same time.
        self.host_bzr = utils.get_host_tool('bzr')

    def get_unique_key(self):
        return [self.original_url, self.tracking, self.ref]

    def get_consistency(self):
        if self.ref is None or self.tracking is None:
            return Consistency.INCONSISTENT

        # Lock for the _check_ref()
        with self._locked():
            if self._check_ref():
                return Consistency.CACHED
            else:
                return Consistency.RESOLVED

    def load_ref(self, node):
        self.ref = self.node_get_member(node, str, 'ref', None)

    def get_ref(self):
        return self.ref

    def set_ref(self, ref, node):
        node['ref'] = self.ref = ref

    def track(self):
        with self.timed_activity("Tracking {}".format(self.url),
                                 silent_nested=True), self._locked():
            self._ensure_mirror(skip_ref_check=True)
            ret, out = self.check_output([self.host_bzr, "version-info",
                                          "--custom", "--template={revno}",
                                          self._get_branch_dir()],
                                         fail="Failed to read the revision number at '{}'"
                                         .format(self._get_branch_dir()))
            if ret != 0:
                raise SourceError("{}: Failed to get ref for tracking {}".format(self, self.tracking))

            return out

    def fetch(self):
        with self.timed_activity("Fetching {}".format(self.url),
                                 silent_nested=True), self._locked():
            self._ensure_mirror()

    def stage(self, directory):
        self.call([self.host_bzr, "checkout", "--lightweight",
                   "--revision=revno:{}".format(self.ref),
                   self._get_branch_dir(), directory],
                  fail="Failed to checkout revision {} from branch {} to {}"
                  .format(self.ref, self._get_branch_dir(), directory))

    def init_workspace(self, directory):
        url = os.path.join(self.url, self.tracking)
        with self.timed_activity('Setting up workspace "{}"'.format(directory), silent_nested=True):
            # Checkout from the cache
            self.call([self.host_bzr, "branch",
                       "--use-existing-dir",
                       "--revision=revno:{}".format(self.ref),
                       self._get_branch_dir(), directory],
                      fail="Failed to branch revision {} from branch {} to {}"
                      .format(self.ref, self._get_branch_dir(), directory))
            # Switch the parent branch to the source's origin
            self.call([self.host_bzr, "switch",
                       "--directory={}".format(directory), url],
                      fail="Failed to switch workspace's parent branch to {}".format(url))

    # _locked()
    #
    # This context manager ensures exclusive access to the
    # bzr repository.
    #
    @contextmanager
    def _locked(self):
        lockdir = os.path.join(self.get_mirror_directory(), 'locks')
        lockfile = os.path.join(
            lockdir,
            utils.url_directory_name(self.original_url) + '.lock'
        )
        os.makedirs(lockdir, exist_ok=True)
        with open(lockfile, 'w') as lock:
            fcntl.flock(lock, fcntl.LOCK_EX)
            try:
                yield
            finally:
                fcntl.flock(lock, fcntl.LOCK_UN)

    def _check_ref(self):
        # If the mirror doesnt exist yet, then we dont have the ref
        if not os.path.exists(self._get_branch_dir()):
            return False

        return self.call([self.host_bzr, "revno",
                          "--revision=revno:{}".format(self.ref),
                          self._get_branch_dir()]) == 0

    def _get_branch_dir(self):
        return os.path.join(self._get_mirror_dir(), self.tracking)

    def _get_mirror_dir(self):
        return os.path.join(self.get_mirror_directory(),
                            utils.url_directory_name(self.original_url))

    def _ensure_mirror(self, skip_ref_check=False):
        mirror_dir = self._get_mirror_dir()
        bzr_metadata_dir = os.path.join(mirror_dir, ".bzr")
        if not os.path.exists(bzr_metadata_dir):
            self.call([self.host_bzr, "init-repo", "--no-trees", mirror_dir],
                      fail="Failed to initialize bzr repository")

        branch_dir = os.path.join(mirror_dir, self.tracking)
        branch_url = self.url + "/" + self.tracking
        if not os.path.exists(branch_dir):
            # `bzr branch` the branch if it doesn't exist
            # to get the upstream code
            self.call([self.host_bzr, "branch", branch_url, branch_dir],
                      fail="Failed to branch from {} to {}".format(branch_url, branch_dir))

        else:
            # `bzr pull` the branch if it does exist
            # to get any changes to the upstream code
            self.call([self.host_bzr, "pull", "--directory={}".format(branch_dir), branch_url],
                      fail="Failed to pull new changes for {}".format(branch_dir))

        if not skip_ref_check and not self._check_ref():
            raise SourceError("Failed to ensure ref '{}' was mirrored".format(self.ref),
                              reason="ref-not-mirrored")


def setup():
    return BzrSource
