# Copyright (C) 2009, 2010 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


import os
from StringIO import StringIO
try:
    from xml.etree.ElementTree import ElementTree
except ImportError:
    # We use bzrlib's version: xml.etree only arrived in Python 2.5
    from bzrlib.util.elementtree.ElementTree import ElementTree

from bzrlib import config, osutils, plugin, registry, trace
from bzrlib.trace import mutter

from bzrlib.plugins.explorer.lib.extensions import (
    extension_type_registry,
    bookmarks,
    tools,
    )
from bzrlib.plugins.explorer.lib.i18n import gettext, N_


def display_name(s):
    """Provide a display friendly name for an accessory directory."""
    return s.replace('_', ' ')


def clothing_names_and_directories():
    """Find the names and directories of available clothes.
    
    :return: list of (name, dir) tuples
    """
    base_dir = clothes_dir()
    result = []
    for name in os.listdir(base_dir):
        fullpath = osutils.pathjoin(base_dir, name)
        if osutils.isdir(fullpath):
            result.append((name, fullpath))
    return sorted(result)


def bag_names_and_directories():
    """Find the names and directories of available bags.
    
    :return: list of (name, dir) tuples
    """
    # Note: We need to decide on an order for searching plugins and
    # alphabetical by name seems as good as any.
    result = []
    plugins = plugin.plugins()
    for name in sorted(plugins):
        plugin_dir = plugins[name].path()
        bag_dir = osutils.pathjoin(plugin_dir, "explorer")
        if os.path.exists(bag_dir):
            result.append((name, bag_dir))
    return result


def hat_names_and_directories():
    """Find the names and directories of available hats."""
    result = []
    # Note: we don't worry about duplicate name detection here.
    # The caller should do that if required.
    for base_dir in hat_path():
        for name in sorted(os.listdir(base_dir)):
            fullpath = osutils.pathjoin(base_dir, name)
            if osutils.isdir(fullpath):
                result.append((name, fullpath))
    return result


def _plugin_dir():
    """Get the root directory for the explorer plugin."""
    this_dir = os.path.dirname(__file__)
    return os.path.dirname(os.path.dirname(this_dir))


def skin_dir():
    """Get the directory holding the core extensions."""
    return osutils.pathjoin(_plugin_dir(), "skin")


def clothes_dir():
    """Get the directory holding the optinal standard extensions."""
    return osutils.pathjoin(_plugin_dir(), "clothes")


def wallet_dir():
    """Get the directory holding personal configuration settings."""
    return osutils.pathjoin(config.config_dir(), "explorer")


def hat_path():
    """Get the list of directories to search for hats."""
    # We search the user's configuration area first, then the explorer plugin
    result = []
    user_dir = osutils.pathjoin(wallet_dir(), "hats")
    std_dir = osutils.pathjoin(_plugin_dir(), "hats")
    for d in [user_dir, std_dir]:
        if os.path.exists(d):
            result.append(d)
    return result


def load_hat(name):
    """Return the hat with a given name or None if name isn't found."""
    for hat, dir in hat_names_and_directories():
        if hat == name:
            return Accessory(dir, name)
    return None


class Accessories(object):
    """A collection of items that provide extension points.
    
    The following attributes are provided:
        
        * skin - an accessory that provides core explorer functionality
        * clothes - optional standard extensions
        * bags - a list of accessories provided by plugins
        * wallet - an accessory holding personal stuff
        * hat - an accessory holding path-specific extensions
    """

    def __init__(self, clothes_to_wear=None, bags_to_blacklist=None,
            hat_switched_callback=None):
        """Create a collection of accessories.

        :param clothes_to_wear: set of optional standard extensions to enable
        :param bags_to_blacklist: set of plugin extensions to skip over
        """
        self.clothes_to_wear = clothes_to_wear
        self.bags_to_blacklist = bags_to_blacklist
        self._hat_switched_callback = hat_switched_callback
        self.reset()
        self.hat_selector = HatSelector()
        self.hat = None
        self._hats = {}
        self._available_hats = None

    def select_hat_for_path(self, path):
        """Select a hat given a directory path.

        :return: the hat to suggest for this location if no hat
          is currently selected, otherwise None.
        """
        result = None
        new_hat = None
        hat_name, path_matched = self.hat_selector.find_hat(path)
        if hat_name:
            try:
                new_hat = self._hats[hat_name]
            except KeyError:
                for hat, dir in hat_names_and_directories():
                    if hat == hat_name:
                        new_hat = Accessory(dir, hat_name)
                        new_hat.load()
                        self._hats[hat_name] = new_hat
                        break
        elif hat_name is None or path != path_matched:
            # Suggest a hat if appropriate
            basename = os.path.basename(path)
            available = self.get_available_hats()
            if available and basename in available:
                result = basename
        if new_hat != self.hat:
            #print "switching to hat %s" % (new_hat,)
            self.hat = new_hat
            self._hat_switched_callback()
        return result

    def get_available_hats(self):
        if self._available_hats is None:
            self._available_hats = \
                set([h for h, d in hat_names_and_directories()])
        return self._available_hats

    def set_clothes_to_wear(self, clothes_to_wear):
        """Update the set of enabled clothes."""
        self.clothes_to_wear = clothes_to_wear
        self.clothes = self._find_clothes(clothes_to_wear)
        self._base_items = [self.skin] + self.clothes + self.bags + \
            [self.wallet]
        self._base_logos = self._find_logos(self._base_items)

    def set_bags_to_blacklist(self, bags_to_blacklist):
        """Update the set of didsabled bags."""
        self.bags_to_blacklist = bags_to_blacklist
        self.bags = self._find_bags(bags_to_blacklist)
        self._base_items = [self.skin] + self.clothes + self.bags + \
            [self.wallet]
        self._base_logos = self._find_logos(self._base_items)

    def reset(self):
        """Reset all attributes.
        
        This is useful when accessories have been added or deleted.
        """
        self.skin = Accessory(skin_dir(), gettext("Skin"))
        self.clothes = self._find_clothes(self.clothes_to_wear)
        self.bags = self._find_bags(self.bags_to_blacklist)
        # The wallet should have all extension types
        required_types = extension_type_registry.keys()
        self.wallet = Accessory(wallet_dir(), gettext("My"), required_types)
        self._base_items = [self.skin] + self.clothes + self.bags + \
            [self.wallet]
        self._base_logos = self._find_logos(self._base_items)

        # Reload the tools and bookmarks in each accessory
        for acc in self._base_items:
            acc.load()

    def _find_clothes(self, include_set):
        if not include_set:
            return []
        mutter("loading explorer extensions for clothes %s" % (include_set,))
        clothes = []
        base_dir = clothes_dir()
        for name in include_set:
            item_dir = osutils.pathjoin(base_dir, name)
            if not os.path.exists(item_dir):
                mutter("Failed to load extensions from %s" % item_dir)
                continue
            title = display_name(name)
            item = Accessory(item_dir, title)
            clothes.append(item)
        return clothes

    def _find_bags(self, exclude_set):
        plugins = plugin.plugins()
        # Note: We need to decide on an order for searching plugins and
        # alphabetical by name seems as good as any.
        bags = []
        for name in sorted(plugins):
            if name in exclude_set:
                continue
            plugin_dir = plugins[name].path()
            bag_dir = osutils.pathjoin(plugin_dir, "explorer")
            if os.path.exists(bag_dir):
                title = display_name(name)
                bag = Accessory(bag_dir, title)
                mutter("explorer extensions provided by the %s plugin in %s"
                    % (name, bag_dir))
                bags.append(bag)
        return bags

    def _find_logos(self, items):
        """Find the map of logo names to logo paths."""
        result = {}
        for acc in items:
            result.update(acc.logos())
        return result

    def base_items(self):
        """Return the list of base accessories - hat always excluded."""
        return self._base_items

    def items(self):
        """Return the list of accessories."""
        if self.hat:
            return self._base_items + [self.hat]
        else:
            return self._base_items

    def logo_path(self, name):
        """Return the path to a logo or None if not found."""
        if self.hat:
            result = self.hat.logos().get(name)
            if result is not None:
                return result
        return self._base_logos.get(name)

    def suggest_hat(self, path):
        """Return the hat to suggest for a location, if any."""
        basename = os.path.basename(path)


class Accessory(object):

    def __init__(self, directory, title, required_types=None):
        """Construct an accessory for Bazaar Explorer.

        :param directory: the directory to load the accessory from or
          create it in.
        :param title: accessory title
        """
        #print "loading %s accessory from %s" % (title, directory)
        self.directory = directory
        self._title = title
        self._icon_path = None
        self._required_types = required_types or []
        if not os.path.exists(directory):
            os.makedirs(self.directory)
        self._create_missing_content()
        self._bookmarks = None
        self._tools = None
        self._toolbars = None

    def __repr__(self):
        return "Hat(%s, %s)" % (self.directory, self._title)

    def title(self):
        return self._title

    def icon_path(self):
        icon_path = osutils.pathjoin(self.directory, "icon.png")
        if os.path.exists(icon_path):
            return icon_path
        else:
            return None

    def _create_missing_content(self):
        parameters = {'title': self._encode_parameter(self.title())}
        for ext_type in self._required_types:
            self._create_content_if_missing(ext_type, parameters)

    def _encode_parameter(self, p):
        if p:
            return p.encode('ascii', 'replace')
        else:
            return p

    def _create_content_if_missing(self, ext_type, parameters):
        ext_type = extension_type_registry.get(ext_type)
        pathname = osutils.pathjoin(self.directory, ext_type.filename)
        template = ext_type.content_template
        if isinstance(template, dict):
            if not os.path.exists(pathname):
                os.mkdir(pathname)
            # Directory of files
            for f, t in template.items():
                filename = osutils.pathjoin(pathname, f)
                self._write_content(filename, t, parameters)
        else:
            # Plain file
            self._write_content(pathname, template, parameters)

    def _write_content(self, pathname, template, parameters):
        # Ignore leading/trailing whitespace in templates
        content = template.strip() % parameters
        if not os.path.exists(pathname):
            try:
                f = open(pathname, "w")
            except OSError, ex:
                mutter("failed to open %s for writing: %s", path, ex)
                return
            try:
                f.write(content)
            except EnvironmentError, ex:
                mutter("failed to write initial content to %s"
                        " with parameters %s: %s", path, parameters, ex)
            f.close()

    def bookmarks(self):
        """Return the BookmarkStore holding bookmark definitions."""
        if self._bookmarks is None:
            self._bookmarks = bookmarks.BookmarkStore(self.bookmarks_path())
        return self._bookmarks

    def tools(self):
        """Return the ToolStore holding tool definitions."""
        if self._tools is None:
            self._tools = tools.ToolStore(self.tools_path())
        return self._tools

    def toolbars(self):
        """Return the ToolStore holding toolbar definitions."""
        if self._toolbars is None:
            self._toolbars = tools.ToolStore(self.toolbars_path())
        return self._toolbars

    def custom_editors(self, desktop_name):
        editors = {}
        cfg_path = self.editors_path()
        cfg = config.ConfigObj(cfg_path, encoding="utf-8")
        for section in ['DEFAULT', desktop_name]:
            try:
                data = cfg[section]
            except KeyError:
                continue
            else:
                for key, value in data.items():
                    exts = key.split(',')
                    for ext in exts:
                        editors[ext.strip()] = value
        return editors

    def load(self):
        self.bookmarks().load()
        self.tools().load()
        self.toolbars().load()

    def logos(self):
        """Return a map of logo names to icon paths."""
        result = {}
        base_path = self.logos_dir()
        if os.path.exists(base_path):
            for filename in os.listdir(base_path):
                name = os.path.splitext(filename)[0]
                result[name] = osutils.pathjoin(base_path, filename)
        return result

    def bookmarks_path(self):
        basename = extension_type_registry.get("bookmarks").filename
        return osutils.pathjoin(self.directory, basename)

    def tools_path(self):
        basename = extension_type_registry.get("tools").filename
        return osutils.pathjoin(self.directory, basename)

    def toolbars_path(self):
        basename = extension_type_registry.get("toolbars").filename
        return osutils.pathjoin(self.directory, basename)

    def editors_path(self):
        basename = extension_type_registry.get("editors").filename
        return osutils.pathjoin(self.directory, basename)

    def logos_dir(self):
        basename = extension_type_registry.get("logos").filename
        return osutils.pathjoin(self.directory, basename)


class HatSelector(object):
    """A hat selector that uses explorer.conf.

    Data is stored in a section called "Hat_Selection_Rules".
    Qt has problems with paths as keys (on Linux at least) so
    we use integers as keys and format the value as path|hat.
    """
    section_name = "Hat_Selection_Rules"

    def __init__(self):
        self._rules = self._load_rules()

    def _load_rules(self):
        from bzrlib.plugins.explorer.lib import explorer_preferences
        result = {}
        settings = explorer_preferences._get_settings()
        settings.beginGroup(self.section_name)
        for key in settings.allKeys():
            value = unicode(settings.value(key).toString())
            path, hat = value.split('|', 1)
            result[path] = hat
        settings.endGroup()
        return result

    def _save_rules(self):
        from PyQt4 import QtCore
        from bzrlib.plugins.explorer.lib import explorer_preferences
        result = {}
        settings = explorer_preferences._get_settings()
        settings.beginGroup(self.section_name)
        for index, name in enumerate(sorted(self._rules)):
            value = "%s|%s" % (name, self._rules[name])
            # This next line is required for some versions of PyQt ...
            value = QtCore.QVariant(value)
            settings.setValue(str(index), value)
        # Check old records are removed
        new_rule_count = len(self._rules.keys())
        old_keys = sorted(settings.allKeys())
        for key in old_keys[new_rule_count:]:
            settings.remove(key)
        settings.endGroup()

    def find_hat(self, path):
        """Return the hat name to use for a path.

        :return: hat name, path matched or None, None.
          An empty string for the hat name means that the user doesn't
          want a hat for this location.
        """
        try:
            return self._rules[path], path
        except KeyError:
            parent = os.path.dirname(path)
            while parent != path:
                path = parent
                try:
                    return self._rules[path], path
                except KeyError:
                    parent = os.path.dirname(path)
        return None, None

    def set_rule(self, path, hat_name):
        """Add or update a hat selection rule.
        
        Configures hat_name as the hat to use for path and its subdirectories.
        """
        self._rules[path] = hat_name
        self._save_rules()

    def clear_rule(self, path):
        """Remove a hat selection rule."""
        del self._rules[path]
        self._save_rules()

    def rules(self):
        """Return the configured rules as a list of tuples."""
        return self._rules


class _ExperimentalHatSelector(object):
    """A hat selector that uses locations.conf.

    This code is untested but may be useful in the future if
    we decide to store the data there instead of explorer.conf.
    """
    setting_name = "explorer.hat"

    def __init__(self):
        self._configs = {}

    def _config_for_path(self, p):
        try:
            return self._configs[path]
        except KeyError:
            config = config.LocationConfig(path)
            self._configs[path] = config
            return config

    def find_hat(self, path):
        """Return the hat name to use for a path or None if none."""
        config = self._config_for_path(path)
        return config.get_user_option(self.setting_name)

    def set_rule(self, path, hat_name):
        """Add or update a hat selection rule.
        
        Configures hat_name as the hat to use for path and its subdirectories.
        """
        config = self._config_for_path(path)
        config.set_user_option(self.setting_name, hat_name)

    def clear_rule(self, path):
        """Remove a hat selection rule."""
        config = self._config_for_path(path)
        config.set_user_option(self.setting_name, None)

    def rules(self):
        """Return the configured rules as a list of tuples."""
        # Hmmm - not sure how best to implement this
        return []


## Toolsets ##

# For now, we define toolsets in a (private) registry. In the future,
# these could become extension objects that accessories might provide.

def find_toolset(name):
    """Get the toolset of a given name or None if not found."""
    return _toolset_registry.get(name)


class Toolset(object):
    """A 'macros' that expands to a set of tool entries."""
    # Note: We explicitly name the object Toolset here. (A ToolSet is
    # an object found within the XML file indicating to use one of these.)

    def __init__(self, name, template):
        self._name = name
        self._template = template
        self.mapper = tools._ETreeToolMapper()

    def as_tool_folder(self, parameters):
        template = '<folder title="Toolset">%s</folder>' % (self._template,)
        text = template % parameters
        file = StringIO(text)
        etree = ElementTree(file=file)
        return self.mapper.etree_to_folder(etree)


# Useful support links for projects on Launchpad
_LP_SUPPORT_TEMPLATE = """
    <tool action="https://launchpad.net/products/%(project)s/+filebug" icon="status/weather-storm" title="Report Bug" type="link" />
    <tool action="https://answers.launchpad.net/%(project)s" icon="apps/help-browser" title="Ask Question" type="link" />
"""

# Useful developer links for projects on Launchpad
_LP_DEVEL_TEMPLATE = """
<tool action="https://answers.launchpad.net/%(project)s/+questions?field.sort=recently+updated+first&amp;field.actions.search=Search&amp;field.status=Open&amp;field.search_text=" icon="apps/help-browser" title="Questions" type="link" />
<tool action="https://bugs.launchpad.net/%(project)s/+bugs?search=Search&amp;field.status=New" icon="status/weather-storm" title="Bugs" type="link" />
<tool action="https://code.launchpad.net/%(project)s/+activereviews" icon="status/weather-few-clouds" title="Reviews" type="link" />
"""

_toolset_registry = registry.Registry()
_toolset_registry.register("lp-support", Toolset("lp-support",
    _LP_SUPPORT_TEMPLATE))
_toolset_registry.register("lp-devel", Toolset("lp-devel",
    _LP_DEVEL_TEMPLATE))
