import json
import re

from trac.admin import IAdminCommandProvider
from trac.attachment import Attachment, IAttachmentChangeListener
from trac.core import Component, implements
from trac.versioncontrol import (
    RepositoryManager, NoSuchChangeset, IRepositoryChangeListener)
from trac.web.api import HTTPNotFound, IRequestHandler, ITemplateStreamFilter

from genshi.builder import tag
from genshi.filters import Transformer

from code_comments.api import ICodeCommentChangeListener
from code_comments.comments import Comments


class Subscription(object):
    """
    Representation of a code comment subscription.
    """
    id = 0
    user = ''
    type = ''
    path = ''
    rev = ''
    repos = ''
    notify = True

    def __init__(self, env, data=None):
        if isinstance(data, dict):
            self.__dict__ = data
        self.env = env

    def __str__(self):
        """
        Returns a user friendly string representation.
        """
        template = "{0} for {1} {2}"
        if self.type == "changeset":
            _identifier = self.rev
        elif self.type == "browser":
            _identifier = "{0} @ {1}".format(self.path, self.rev)
        else:
            _identifier = self.path

        return template.format(self.user, self.type, _identifier)

    @classmethod
    def select(cls, env, args={}, notify=None):
        """
        Retrieve existing subscription(s).
        """
        select = 'SELECT * FROM code_comments_subscriptions'

        if notify:
            args['notify'] = bool(notify)

        if len(args) > 0:
            select += ' WHERE '
            criteria = []
            for key, value in args.iteritems():
                template = '{0}={1}'
                if isinstance(value, basestring):
                    template = '{0}=\'{1}\''
                if (isinstance(value, tuple) or isinstance(value, list)):
                    template = '{0} IN (\'{1}\')'
                    value = '\',\''.join(value)
                if isinstance(value, bool):
                    value = int(value)
                criteria.append(template.format(key, value))
            select += ' AND '.join(criteria)

        cursor = env.get_read_db().cursor()
        cursor.execute(select)
        for row in cursor:
            yield cls._from_row(env, row)

    def insert(self, db=None):
        """
        Insert a new subscription. Returns bool to indicate success.
        """
        if self.id > 0:
            # Already has an id, don't insert
            return False
        else:
            with self.env.db_transaction as db:
                cursor = db.cursor()
                insert = ("INSERT INTO code_comments_subscriptions "
                          "(user, type, path, repos, rev, notify) "
                          "VALUES (%s, %s, %s, %s, %s, %s)")
                values = (self.user, self.type, self.path, self.repos,
                          self.rev, self.notify)
                cursor.execute(insert, values)
                self.id = db.get_last_id(cursor, 'code_comments_subscriptio
ns')
                return True

    def update(self, db=None):
        """
        Update an existing subscription. Returns bool to indicate success.
        """
        if self.id == 0:
            # Doesn't have a valid id, don't update
            return False
        else:
    
        with self.env.db_transaction as db:
                cursor = db.cursor()
                update = ("UPDATE code_comments_subscriptions SET "
                          "user=%s, type=%s, path=%s, repos=%s, rev=%s, "
                          "notify=%s WHERE id=%s")
                values = (self.user, self.type, self.path, self.repos,
                          self.rev, self.notify, self.id)
                try:
                    cursor.execute(update, values)
                except db.IntegrityError:
                    self.env.log.warning("Subscription update failed.")
                    return False
                return True

    def delete(self):
        """
        Delete an existing subscription.
        """
        if self.id > 0:
            with self.env.db_transaction as db:
                cursor = db.cursor()
                delete = ("DELETE FROM code_comments_subscriptions WHERE "
                          "id=%s")
                cursor.execute(delete, (self.id,))

    @classmethod
    def _from_row(cls, env, row):
        """
        Creates a subscription from a list (representing a database row).
        """
        try:
            subscription = cls(env)
            subscription.id = int(row[0])
            subscription.user = row[1]
            subscription.type = row[2]
            subscription.path = row[3]
            subscription.repos = row[4]
            subscription.rev = row[5]
            subscription.notify = bool(row[6])
            return subscription
        except IndexError:
            # Invalid row
            return None

    @classmethod
    def _from_dict(cls, env, dict_, create=True):
        """
        Retrieves or (optionally) creates a subscription from a dict.
        """
        subscription = None

        # Look for existing subscriptions
        args = {
            'user': dict_['user'],
            'type': dict_['type'],
            'path': dict_['path'],
            'repos': dict_['repos'],
            'rev': dict_['rev'],
        }
        subscriptions = cls.select(env, args)

        # Only return the first one
        for _subscription in subscriptions:
            if subscription is None:
                subscription = _subscription
                env.log.info('Subscription found: [%d] %s',
                             subscription.id, subscription)
            else:
                # The unique constraint on the table should prevent this ever
                # occurring
                env.log.warning('Multiple subscriptions found: [%d] %s',
                                subscription.id, subscription)

        # (Optionally) create a new subscription if we didn't find one
        if subscription is None and create:
            subscription = cls(env, dict_)
            subscription.insert()
            env.log.info('Subscription created: [%d] %s',
                         subscription.id, subscription)

        return subscription

    @classmethod
    def from_attachment(cls, env, attachment, user=None, notify=True):
        """
        Creates a subscription from an Attachment object.
        """
        _path = "/{0}/{1}/{2}".format(attachment.parent_realm,
                                      attachment.parent_id,
                                      attachment.filename)

        sub = {
            'user': user or attachment.author,
            'type': 'attachment',
            'path': _path,
            'repos': '',
            'rev': '',
            'notify': notify,
        }
        return cls._from_dict(env, sub)

    @classmethod
    def from_changeset(cls, env, changeset, user=None, notify=True):
        """
        Creates a subscription from a Changeset object.
        """
        sub = {
            'user': user or changeset.author,
            'type': 'changeset',
            'path': '',
            'repos': changeset.repos.reponame,
            'rev': changeset.rev,
            'notify': notify,
        }
        return cls._from_dict(env, sub)

    @classmethod
    def from_comment(cls, env, comment, user=None, notify=True):
        """
        Creates a subscription from a Comment object.
        """
        sub = {
            'user': user or comment.author,
            'type': comment.type,
            'notify': notify,
        }

        # Munge attachments
        if comment.type == 'attachment':
            sub['path'] = comment.path.split(':')[1]
            sub['repos'] = ''
            sub['rev'] = ''

        # Munge changesets and browser
        if comment.type in ('changeset', 'browser'):
            if comment.type == 'browser':
                sub['path'] = comment.path
            else:
                sub['path'] = ''
            repo = RepositoryManager(env).get_repository(None)
            try:
                sub['r