# Copyright (C) 2009 Canonical Ltd
#
# 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 St, Fifth Floor, Boston, MA 02110-1301  USA

"""Initialization of a repository, branch and tree in various ways."""


from bzrlib.lazy_import import lazy_import
lazy_import(globals(), '''

import os

from bzrlib import (
    builtins,
    bzrdir,
    commands,
    errors,
    osutils,
    registry,
    trace,
    transport,
    )
from bzrlib.trace import note, mutter
from bzrlib.plugins.explorer.lib.helpers import plural
''')
from bzrlib.option import Option, RegistryOption


class MissingParentDirectory(errors.BzrError):

    _fmt = "Parent directory for %(url)s does not exist."

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


class UnknownWorkspaceModel(errors.BzrError):

    _fmt = "Unknown workspace model %(model)s"

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


def init_workspace(location, model=None, format=None, create_parent_dirs=False,
    outf=None, model_params=None):
    """Initialise a workspace.
    
    :param location: base location of the workspace
    :param model: the workspace model to use
    :param format: storage format to use
    :param create_parent_dirs: create parent directories of location if
      they don't already exist
    :param outf: output stream; if None, standard output is used
    :param model_params: a dictionary of model-specific parameters
    """
    # Assign defaults
    if model is None:
        model = workspace_model_registry.get()
    if format is None:
        format = bzrdir.format_registry.make_bzrdir('default')
    if outf is None:
        outf = sys.stdout
    mutter("initialising workspace %s using model %s" % (location, model))

    # Create the parent directories if required
    _ensure_base_of_location(location, create_parent_dirs)

    # Build the workspace
    ws_model = model()
    if model_params:
        model_params = dict([(k.replace('-', '_'), v)
            for k, v in model_params.items()])
    else:
        model_params = {}
    repo_dir, branch_dir, tree_dir = ws_model.build(outf, location,
        format, **model_params)

    # Move existing files if this is required
    #if tree_dir != location:
    #    mutter("moving files from %s to %s" % (location, tree_dir))


def _ensure_base_of_location(location, create_parent_dirs):
    to_transport = transport.get_transport(location)
    try:
        to_transport.ensure_base()
    except errors.NoSuchFile:
        # The path has to exist to initialize a repo or branch inside of it.
        # Raise an exception the GUI can easily trap unless we were
        # explicitly asked to create parent directories.
        if not create_parent_dirs:
            raise MissingParentDirectory(location)
        to_transport.create_prefix()


## Helper routines for building workspaces ##

def init_repository(outf, location, format, no_trees=False):
    """Initialise a repository."""
    old_verbosity = trace.get_verbosity_level()
    trace.set_verbosity_level(-1)
    try:
        core_cmd = _get_command_object('init-repository', outf)
        core_cmd.run(location, format=format, no_trees=no_trees)
    finally:
        trace.set_verbosity_level(old_verbosity)
    if no_trees:
        note("Created repository with treeless branches at %s" % (location,))
    else:
        note("Created repository at %s" % (location,))


def init_branch(outf, location, format):
    """Initialise a branch."""
    old_verbosity = trace.get_verbosity_level()
    trace.set_verbosity_level(-1)
    try:
        core_cmd = _get_command_object('init', outf)
        core_cmd.run(location, format=format)
    finally:
        trace.set_verbosity_level(old_verbosity)
    note("Created branch at %s" % (location,))


def init_working_tree(outf, branch_location, to_location):
    """Initialise a lightweight checkout to use as a working tree."""
    old_verbosity = trace.get_verbosity_level()
    trace.set_verbosity_level(-1)
    try:
        core_cmd = _get_command_object('checkout', outf)
        core_cmd.run(branch_location, to_location, lightweight=True)
    finally:
        trace.set_verbosity_level(old_verbosity)
    note("Created working tree at %s" % (to_location,))


def move_files(from_location, to_location, rel_paths):
    """Move nominated files."""
    note("Moving files from %s to %s ..." % (from_location, to_location,))
    dirs = 0
    files = 0
    links = 0
    for rp in rel_paths:
        old_path = osutils.pathjoin(from_location, rp)
        new_path = osutils.pathjoin(to_location, rp)
        try:
            os.rename(old_path, new_path)
            note("%s" , rp)
        except OSError, ex:
            note("Unable to move %s - %s." % (old_path, ex))
        else:
            if osutils.isdir(new_path):
                dirs += 1
            elif osutils.islink(new_path):
                links += 1
            else:
                files += 1
    note("Moved %d %s, %d %s and %d %s." % (
        dirs, plural(dirs, "directory", "directories"),
        files, plural(files, "file", "files"),
        links, plural(links, "link", "links")))


def _get_command_object(cmd_name, outf):
    cmd_obj = commands.get_cmd_object(cmd_name)
    # We explicitly reuse the current command's output stream rather than
    # setup a fresh one here
    cmd_obj.outf = outf
    return cmd_obj


## The workspace model registry ##

class WorkspaceModel(object):
    """Abstract workspace model.
    
    The following attributes may be defined:
        
    * title - the label for this model in the GUI
    * just_repository - if True, this model only creates a repository
    * parameters - a list of model-specific options
    """

    title = "???"
    just_repository = False
    parameters = []
    
    def build(self, outf, location, format, **model_params):
        """Build a workspace.

        :param outf: the output file for reporting actions
        :param location: the location of the workspace
        :param format: the format to use
        :param model_params: model-specific parameters
        :return: repo_dir, branch_dir, tree_dir - the directories
            created for the various parts, if any.
        """
        raise NotImplementedError(WorkspaceModel.build)


class WorkspaceModelRegistry(registry.Registry):
    """Registry for workspace models.
    
    The registered object should be a WorkspaceModel.
    The doc-string for the registered object will be used as help by default.
    """

    def get_help(self, key=None):
        result = registry.Registry.get_help(self, key)
        if result is None:
            from inspect import getdoc
            model = self.get(key)
            result = getdoc(model)
        return result


workspace_model_registry = WorkspaceModelRegistry()


## Standard workspace models ##

class FeatureBranchesWorkspaceModel(WorkspaceModel):
    """Create a shared repository at location then create a trunk branch inside it."""

    title = "Feature branches"
    parameters = [
             Option('move-existing',
                 help='Move existing files, if any, to working tree location.'),
             Option('with-shared-tree',
                 help='Create a work checkout to share across branches.'),
            ]

    def build(self, outf, location, format, move_existing=False,
            with_shared_tree=False):
        if move_existing:
            existing_files = os.listdir(location)
        trunk_location = osutils.pathjoin(location, 'trunk')
        if with_shared_tree:
            work_location = osutils.pathjoin(location, 'work')
            init_repository(outf, location, format, no_trees=True)
            init_branch(outf, trunk_location, format)
            init_working_tree(outf, trunk_location, work_location)
        else:
            init_repository(outf, location, format)
            init_branch(outf, trunk_location, format)
            work_location = trunk_location
        if move_existing and existing_files:
            move_files(location, work_location, existing_files)
        return location, trunk_location, work_location


class ColocatedBranchesWorkspaceModel(WorkspaceModel):
    """Create a working tree at location pointing to a trunk branch
    in an embedded (no-trees) shared repository.
    """

    title = "Colocated branches"
    parameters = [
             Option('bzrbranches',
                 help="Put repository in .bzrbranches (instead of .bzr/branches)."),
            ]

    def build(self, outf, location, format, bzrbranches=False):
        if bzrbranches:
            repo_location = osutils.pathjoin(location, ".bzrbranches")
            trunk_location = osutils.pathjoin(repo_location, 'trunk')
            init_repository(outf, repo_location, format, no_trees=True)
            init_branch(outf, trunk_location, format)
            init_working_tree(outf, trunk_location, location)
        else:
            # Note: These os calls should become transport calls to
            # ensure this works on non-local transports ...
            bzr_location = osutils.pathjoin(location, ".bzr")
            os.mkdir(bzr_location)
            repo_location = osutils.pathjoin(bzr_location, "branches")
            trunk_location = osutils.pathjoin(repo_location, 'trunk')
            init_repository(outf, repo_location, format, no_trees=True)
            init_branch(outf, trunk_location, format)
            # Move the .bzr directory aside so we can create the checkout
            tmp_location = osutils.pathjoin(location, ".bzr-tmp")
            os.rename(bzr_location, tmp_location)
            try:
                tmp_repo = osutils.pathjoin(tmp_location, "branches")
                tmp_trunk = osutils.pathjoin(tmp_repo, "trunk")
                init_working_tree(outf, tmp_trunk, location)
                # Now move the repository into the .bzr directory.
                os.rename(tmp_repo, repo_location)
            finally:
                os.rmdir(tmp_location)
            # Update the checkout reference
            self._switch_checkout(location, trunk_location)
        return repo_location, trunk_location, location

    def _switch_checkout(self, checkout_location, branch_location):
        from bzrlib.branch import Branch
        control = bzrdir.BzrDir.open(checkout_location)
        to_branch = Branch.open(branch_location)
        try:
            control.find_branch_format().set_reference(control, to_branch)
        except TypeError:
            # API change in 2.2
            control.find_branch_format().set_reference(control, None,
                                                       to_branch)


class PlainBranchWorkspaceModel(WorkspaceModel):
    """Create a plain branch at location;
    if inside a shared repository, that will be used,
    otherwise the branch will be a standalone branch.
    """

    title = "Plain branch"

    def build(self, outf, location, format):
        init_branch(outf, location, format)
        return None, location, location


class SharedRepoWorkspaceModel(WorkspaceModel):
    """Create a shared repository at location."""

    title = "Shared repository"
    just_repository = True
    parameters = [
             Option('no-trees',
                 help='Branches in the repository will default to'
                      ' not having a working tree.'),
            ]

    def build(self, outf, location, format, no_trees=False):
        init_repository(outf, location, format, no_trees=no_trees)
        return location, None, None


workspace_model_registry.register("feature-branches",
    FeatureBranchesWorkspaceModel)
workspace_model_registry.register("colocated-branches",
        ColocatedBranchesWorkspaceModel)
workspace_model_registry.register("plain-branch", PlainBranchWorkspaceModel)
workspace_model_registry.register("shared-repository", SharedRepoWorkspaceModel)
workspace_model_registry.default_key = "feature-branches"
