# 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

"Bookmark management."""

import os, sys
import urllib
try:
    from HTMLParser import HTMLParser
    _htmlparser_available = True
except ImportError:
    _htmlparser_available = False

from bzrlib import osutils, trace, debug
try:
    from xml.etree.ElementTree import ElementTree, Element
except ImportError:
    # We use bzrlib's version: xml.etree only arrived in Python 2.5
    from bzrlib.util.elementtree.ElementTree import ElementTree, Element


# Title for folder holding the stuff we care about
BOOKMARKS_ROOT_TITLE = "Bazaar Bookmarks"


class BookmarkEntry(object):
    """Any item that can appear in a bookmark collection."""


class Bookmark(BookmarkEntry):
    """A bookmark.
    
    After creating, access data using the following properties:

    * title - text to display
    * location - URL
    * kind - the kind of bookmark ("branch", "repository", etc.)
    """

    def __init__(self, title, location, kind=None):
        self.title = title
        self.location = location
        self.kind = kind

    def __repr__(self):
        return "Bookmark(%s, %s)" % (self.title, self.location)


class BookmarkFolder(BookmarkEntry):
    """A directory of bookmark entries."""

    def __init__(self, title):
        self.title = title
        self._children = []

    def __repr__(self):
        return "BookmarkFolder(%s)" % (self.title,)

    def append(self, entry):
        self._children.append(entry)

    def __len__(self):
        return len(self._children)

    def __iter__(self):
        return iter(self._children)


class BookmarkSeparator(BookmarkEntry):
    """A separator between bookmarks."""

    def __repr__(self):
        return "BookmarkSeparator()"


class BookmarkStore(object):
    """A persistent store of bookmarks."""

    def __init__(self, path=None):
        self._path = path
        self.mapper = _ETreeBookmarkMapper()
        self.load()

    def load(self):
        """Load the collection."""
        if self._path is None or not os.path.exists(self._path):
            self._root = BookmarkFolder(BOOKMARKS_ROOT_TITLE)
        else:
            try:
                etree = ElementTree(file=self._path)
            except Exception:
                trace.mutter("failed to parse bookmarks file %s" % (self._path,))
                if 'error' in debug.debug_flags:
                    trace.report_exception(sys.exc_info(), sys.stderr)
                self._root = BookmarkFolder(BOOKMARKS_ROOT_TITLE)
            else:
                self._root = self.mapper.etree_to_folder(etree)

    def save(self, path=None):
        """Save the collection.
        
        :param path: if non-None, the path to save to
          (instead of the default path set in the constructor)
        """
        if path is None:
            path = self._path

        # Backup old file if it exists
        if os.path.exists(path):
            backup_path = "%s.bak" % (path,)
            if os.path.exists(backup_path):
                os.unlink(backup_path)
            osutils.rename(path, backup_path)

        # Convert to xml and dump to a file
        etree = self.mapper.folder_to_etree(self._root)
        etree.write(path)
    
    def root(self):
        """Return the root folder of this collection."""
        return self._root

    def import_from(self, path, folder_title=BOOKMARKS_ROOT_TITLE):
        """Import bookmarks from Mozilla's bookmark format.
        
        :param path: the path to import bookmarks from.
        :param folder_name: the name of the folder to import. If None,
          the "Bazaar Bookmarks" folder is imported.
        """
        root = BookmarkFolder(folder_title)
        parser = _MozillaBookmarkImporter(path, root)
        self._root = root


def _etree_indent(elem, level=0):
    # From http://effbot.org/zone/element-lib.htm
    i = "\n" + level*"  "
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + "  "
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
        for elem in elem:
            _etree_indent(elem, level+1)
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
    else:
        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = i


class _ETreeBookmarkMapper(object):

    def folder_to_etree(self, bm_folder):
        """Convert a BookmarkFolder to an ElementTree.

        :param bm_folder: the BookmarkFolder to convert
        :return: the ElementTree generated
        """
        root = self._entry_to_element(bm_folder)
        # Make the output editable (by xml standards at least)
        _etree_indent(root)
        return ElementTree(root)

    def _entry_to_element(self, entry):
        if isinstance(entry, Bookmark):
            element = Element('bookmark',
                title=entry.title,
                location=entry.location)
            if entry.kind is not None:
                element.attrib['kind'] = entry.kind
            return element
        elif isinstance(entry, BookmarkFolder):
            folder_element = Element('folder', title=entry.title)
            for child in entry:
                child_element = self._entry_to_element(child)
                folder_element.append(child_element)
            return folder_element
        elif isinstance(entry, BookmarkSeparator):
            return Element('separator')
        else:
            raise AssertionError("unexpected entry %r" % (entry,))

    def etree_to_folder(self, etree):
        """Convert an ElementTree to a BookmarkFolder.

        :param etree: the ElementTree to convert
        :return: the BookmarkFolder generated
        """
        root = etree.getroot()
        return self._element_to_entry(root)

    def _element_to_entry(self, element):
        tag = element.tag
        if tag == 'bookmark':
            title = element.get('title')
            location = element.get('location')
            kind = element.get('kind')
            return Bookmark(title, location, kind)
        elif tag == 'folder':
            title = element.get('title')
            folder = BookmarkFolder(title)
            for child in element:
                child_entry = self._element_to_entry(child)
                folder.append(child_entry)
            return folder
        elif tag == 'separator':
            return BookmarkSeparator()
        else:
            raise AssertionError("unexpected element tag %s" % (tag,))


if _htmlparser_available:
    class _MozillaBookmarkImporter(HTMLParser):
        """An importer of bookmark data from Mozilla's format."""

        # We explicitly use a HTML parser here so that bookmarks edited by
        # Firefox's bookmark organizer importing/exporting can still be loaded.
        # (The format used by Firefox is not well-formed xml so that rules out
        # using ElementTree unfortunately.)

        def __init__(self, path, folder):
            HTMLParser.__init__(self)
            self._path = path
            self.folders = [folder]
            self.do_parsing()

        def do_parsing(self):
            # init state tracking
            self.found_root = False
            self.processing = None

            # open the file and do the parsing
            url = urllib.pathname2url(self._path)
            data = urllib.urlopen(url)
            self.feed(data.read())
            self.close()

        def trace(self, msg):
            indent = len(self.folders) - 1
            #print "%s%s" % (indent * '  ', msg)

        def append_entry(self, entry):
            """Add a BookmarkEntry to the current folder."""
            self.trace("creating %r" % (entry,))
            self.folders[-1].append(entry)

        def handle_starttag(self, tag, attrs):
            self.last_tag = tag
            self.last_attrs = attrs
            if not self.found_root:
                return
            if not self.processing:
                if tag in ['a', 'h3', 'hr']:
                    if tag == 'a':
                        self.processing = tag
                        self.this_title = ''
                        attrs_dict = dict(attrs)
                        self.this_location = attrs_dict.get('href')
                    elif tag == 'h3':
                        self.this_title = ''
                        self.processing = tag
                    elif tag == 'hr':
                        self.append_entry(BookmarkSeparator())


        def handle_endtag(self, tag):
            if not self.found_root:
                return
            if tag == 'dl':
                #self.trace("closing folder")
                self.folders.pop()
                if len(self.folders) == 0:
                    self.found_root = False
                return
            if not self.processing:
                return
            if tag == 'a':
                # save a bookmark
                self.append_entry(Bookmark(self.this_title, self.this_location))
            elif tag == 'h3':
                # save a folder
                bm = BookmarkFolder(self.this_title)
                self.append_entry(bm)
                self.folders.append(bm)
            self.processing = None

        def handle_data(self, data):
            if not self.found_root:
                if data == BOOKMARKS_ROOT_TITLE and self.last_tag == 'h3':
                    self.found_root = True
                return
            if self.processing in ['a', 'h3']:
                self.this_title += data.rstrip()

        def handle_charref(self, name):
            if not self.found_root:
                return
            if self.processing:
                char = chr(int(name))
                self.this_title += char


# testing
if __name__ == '__main__':
    store = BookmarkStore("/home/ian/Desktop/bm2-in.xml")
    store.save("/home/ian/Desktop/bm2-out.xml")
