# -*- coding: iso8859-1 -*-
#
# Copyright (C) 2005 Edgewall Software
# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://trac.edgewall.com/license.html.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://projects.edgewall.com/trac/.
#
# Author: Jason Parks <jparks@jparks.net>
#         Thomas Tressieres <thomas.tressieres@free.fr>

from __future__ import generators

from trac.util import TracError
from trac.versioncontrol import Changeset, Node, Repository

import p4, os


TmpFileName = os.tempnam() + "_perforce_output.bin"

def _normalize_path(path):
    """
    Return a canonical representation of path in the repos.
    """
    return path + "..."


class PerforceStream(object):
    """
    Wrapper object
    """

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

    def read(self, amt=None):
        return self.content[:amt]



class PerforceRepository(Repository):
    """
    Repository implementation for perforce
    """

    def __init__(self, name, authz, log, port, user, client, passwd, maxItems):
        Repository.__init__(self, name, authz, log)
        self.p4c = p4.P4()
        self.p4c.port = port
        self.p4c.user = user
        self.p4c.client = client
        self.p4c.password = passwd
        self.p4c.parse_forms()
        try:
            self.p4c.connect()
            # cache the first few changes
            self.history = []
            changes = self.p4c.run("changes", "-m", maxItems, "-s", "submitted")
            for change in changes:
                self.history.append(change['change'])

        except self.p4c.P4Error:
            for e in p4.errors:
                self.log.debug(e)


    def close(self):
        """
        Close the connection to the repository.
        """
        raise NotImplementedError


    def get_changeset(self, rev):
        """
        Retrieve a Changeset object that describes the changes made in revision 'rev'.
        """
        #self.log.debug("*** get_changeset rev = %s" % (rev))
        change = { }
        try:
            if rev != None:
                change = self.p4c.run_describe(rev)[0]
            else:
                young = self.get_youngest_rev()
                change = self.p4c.run_describe(young)[0]
        except self.p4c.P4Error:
            for e in p4.errors:
                self.log.debug(e)
        return PerforceChangeset(self.p4c, rev, change, self.log)


    def has_node(self, path, rev):
        """
        Tell if there's a node at the specified (path,rev) combination.
        """
        #self.log.debug("*** has_node %s   %s" % (path, rev))
        try:
            self.get_node()
            return True
        except TracError:
            return False


    def get_node(self, path, rev=None):
        """
        Retrieve a Node (directory or file) from the repository at the
        given path. If the rev parameter is specified, the version of the
        node at that revision is returned, otherwise the latest version
        of the node is returned.
        """
        #self.log.debug("*** get_node path = '%s' rev = %s" % (path, rev))
        if path != '/':
            if path.startswith("//") == False:
                path = path.rstrip('/')
                path = '/' + path

            if path.endswith("...") == True:
                path2 = path.rstrip('...')
                dir = self.p4c.run("dirs", path2)
            else:
                dir = self.p4c.run("dirs", path)

            if len(dir) != 0:
                kind = Node.DIRECTORY
            else:
                kind = Node.FILE
        else:
            kind = Node.DIRECTORY
        return PerforceNode(path, rev, self.p4c, self.log, kind)


    def get_oldest_rev(self):
        #self.log.debug("*** get_oldest_rev rev = %s" % (self.history[-1]))
        return self.history[-1]


    def get_youngest_rev(self):
        """
        Return the youngest revision in the repository.
        """
        rev = self.p4c.run("changes", "-m", "1", "-s", "submitted")[0]['change']
        #self.log.debug("*** get_youngest_rev rev = %s" % (rev))

        if rev != self.history[0]:
            count = int(rev) - int(self.history[0])
            changes = self.p4c.run("changes", "-m", count, "-s", "submitted")
            idx = 0
            for change in changes:
                num = change['change']
                if rev != num:
                    #self.log.debug("*** inserting change %s into history at %d" % (num, idx))
                    self.history.insert(idx, num)
                    idx += 1
                else:
                    break
        return rev


    def previous_rev(self, rev):
        """
        Return the revision immediately preceding the specified revision.
        """
        #self.log.debug("*** previous_rev rev = %s" % (rev))
        idx = self.history.index(rev)
        if idx + 1 < len(self.history):
            return self.history[idx + 1]
        return None


    def next_rev(self, rev):
        """
        Return the revision immediately following the specified revision.
        """
        #self.log.debug("*** next_rev rev = %s" % (rev))
        idx = self.history.index(rev)
        if idx > 0:
            return self.history[idx - 1]
        return None


    def rev_older_than(self, rev1, rev2):
        """
        Return True if rev1 is older than rev2, i.e. if rev1 comes before rev2
        in the revision sequence.
        """
        #self.log.debug("rev_older_than =  %s %s" % (rev1, rev2))
        raise NotImplementedError


    def get_path_history(self, path, rev=None, limit=None):
        """
        Retrieve all the revisions containing this path (no newer than 'rev').
        The result format should be the same as the one of Node.get_history()
        """
        #self.log.debug("get_path_history =  %s %s %s" % (path, rev, limit))
        raise NotImplementedError


    def normalize_path(self, path):
        """
        Return a canonical representation of path in the repos.
        """
        #self.log.debug("normalize_path =  %s" % (path))
        if path != '/':
            if path.startswith("//") == False:
                path = path.rstrip('/')
                path = '/' + path
            dir = self.p4c.run("dirs", path)
            if len(dir) != 0:
                kind = Node.DIRECTORY
            else:
                kind = Node.FILE
        else:
            kind = Node.DIRECTORY
        if kind == Node.DIRECTORY:
            return path + "/..."
        return path


    def normalize_rev(self, rev):
        """
        Return a canonical representation of a revision in the repos.
        'None' is a valid revision value and represents the youngest revision.
        """
        if rev == None:
            rev = self.get_youngest_rev()
        elif rev > self.get_youngest_rev():
            raise TracError, "Revision %s doesn't exist yet" % rev
        #self.log.debug("normalize_rev =  %s" % (rev))
        return rev
        



class PerforceNode(Node):
    """
    Represents a directory or file in the repository.
    """
    def __init__(self, path, rev, p4c, log, kind):
        self.p4c = p4c
        self.log = log

        Node.__init__(self, path, rev, kind)

        if self.isfile:
            self.content = None
            self.info = self.p4c.run("files", path)[0]


    def _get_content(self):
        if self.rev == None:
            cmd = self.path + '#head'
        else:
            cmd = self.path + '@' + self.rev

        type = self.p4c.run("fstat", cmd)
        if type[0]['headType'].startswith('binary') == True:
            file = self.p4c.run("print", "-o", TmpFileName, cmd)
            f = open(TmpFileName, 'rb')
            self.content = f.read()
            f.close()
        else:
            file = self.p4c.run("print", cmd)
            del file[0]
            sep = '\n'
            self.content = sep.join(file)
        #self.log.debug("*** content =  %s" % (self.content))
        return self.content


    def get_content(self):
        """
        Return a stream for reading the content of the node. This method
        will return None for directories. The returned object should provide
        a read([len]) function.
        """
        if self.isdir:
            return None
        return PerforceStream(self._get_content()) 


    def get_entries(self):
        """
        Generator that yields the immediate child entries of a directory, in no
        particular order. If the node is a file, this method returns None.
        """
        #self.log.debug("*** get_entries for '%s' kind = %s" % (self.path, self.kind))
        if self.isfile:
            return
        path = self.path + "/*"
        dirs = self.p4c.run("dirs", path)
        #self.log.debug("---    dirs = '%s'" % (dirs))	
        
        for dir in dirs:
            myDir = dir['dir'] + "..."
            logs = self.p4c.run("fstat", myDir)
            revs = []
            for myLog in logs:
                newRev = int(myLog['headChange'])
                if not newRev in revs:
                    revs.append(newRev)
            revs.sort()

            yield PerforceNode(dir['dir'], str(revs[-1]), self.p4c, self.log, Node.DIRECTORY) 

        if self.path != '/':
            files = self.p4c.run("files", path)
            for file in files:
                #self.log.debug("found file '%s'" % (file['depotFile']))
                change = self.p4c.run("fstat", file['depotFile'])[0]
                rev = change['headChange']
                if change['headAction'] != 'delete':
                    yield PerforceNode(file['depotFile'], rev, self.p4c, self.log, Node.FILE)


    def get_history(self, limit=None):
        """
        Generator that yields (path, rev, chg) tuples, one for each revision in which
        the node was changed. This generator will follow copies and moves of a
        node (if the underlying version control system supports that), which
        will be indicated by the first element of the tuple (i.e. the path)
        changing.
        Starts with an entry for the current revision.
        """
        histories = []

        cmd = _normalize_path(self.path)
        #self.log.debug("*** get_history = %s  %s" % (cmd, limit))
        if self.isfile:
            logs = self.p4c.run("filelog", "-m", str(limit), cmd)
            #self.log.debug("*** get_history logs %s" % (logs))
            index = 0
            while index < len(logs[0]['rev']):
                chg = Changeset.EDIT
                path = self.path
                rev = logs[0]['change'][index]
                action = logs[0]['action'][index]
    
                if action == 'add':
                    chg = Changeset.ADD
                elif action == 'integrate':
                    chg = Changeset.COPY
                elif action == 'branch':
                    chg = Changeset.COPY
                    histories.append([path, rev, chg])
                    path = logs[0]['file'][index][0]
                    chg = Changeset.EDIT
                    rev = str(int(rev) - 1)
                elif action == 'delete':
                    chg = Changeset.DELETE
                
                histories.append([path, rev, chg])
                index += 1
        else:
            logs = self.p4c.run("fstat", cmd)
            index = 0
            revs = []
            for myLog in logs:
                newRev = myLog['headChange']
                if newRev in revs:
                    pass
                else:
                    revs.append(newRev)
            revs.sort()
            revs.reverse()

            for rev in revs:
                histories.append([self.path, rev, Changeset.EDIT])
                index += 1

        for c in histories:
            yield tuple(c)


    def get_properties(self):
        """
        Returns a dictionary containing the properties (meta-data) of the node.
        The set of properties depends on the version control system.
        """
        #self.log.debug("*** get_properties = %s rev=%s" % (self.path, self.rev))
        return { }


    def get_content_length(self):
        if self.isdir:
            return None
        type = self.p4c.run("fstat", "-Ol", self.path)
        if type[0]['headAction'].startswith('delete') == True:
            return 0
        #self.log.debug("*** get_content_length = %s" % type)
        return int(type[0]['fileSize'])


    def get_content_type(self):
        #self.log.debug("*** get_content_type = %s  rev = %s" % (self.path, self.rev))
        if self.isdir:
            return None
            change = self.p4c.run("fstat", self.path)[0]
            if change['headType'].startswith('binary') == True:
                return 'application/octet-stream'
        return None


    def get_last_modified(self):
        #self.log.debug("*** get_last_modified = %s" % self.path)
        return int(self.info['time'])



class PerforceChangeset(Changeset):
    """
    Represents a set of changes of a repository.
    """

    def __init__(self, p4c, rev, change, log):
        self.log = log
        self.rev = rev
        self.change = change
        self.p4c = p4c
        message = ""
        author = ""
        date = 0
        #self.log.debug("*** changeset init = %s  rev = %s" % (change, rev))
        if len(change) != 0: 
            message = self.change['desc']	
            author = self.change['user']
            date = int(self.change['time'])
        Changeset.__init__(self, rev, message, author, date)

    def get_changes(self):
        """
        Generator that produces a (path, kind, change, base_path, base_rev)
        tuple for every change in the changeset, where change can be one of
        Changeset.ADD, Changeset.COPY, Changeset.DELETE, Changeset.EDIT or
        Changeset.MOVE, and kind is one of Node.FILE or Node.DIRECTORY.
        """
        #self.log.debug("*** get_changes = %s" % (self.change))
        files = self.change['depotFile']
        changes = []
        
        index = 0
        for file in files:
            #rev = self.change['rev'][index]
            rev = str(int(self.rev) - 1)
            action = self.change['action'][index]
            #self.log.debug("*** get_changes %s %s %s" % (file, action, rev))

            if action == 'integrate':
                filelog = self.p4c.run("filelog", "-m", "1", file)
                action = Changeset.COPY
                changes.append([file, Node.FILE, action, filelog[0]['file'][0][0], rev])
            else:
                changes.append([file, Node.FILE, action, file, rev])
            index += 1

        for c in changes:
            yield tuple(c)


