### Copyright (C) 2002-2005 Stephen Kennedy <stevek@gnome.org>

### Redistribution and use in source and binary forms, with or without
### modification, are permitted provided that the following conditions
### are met:
### 
### 1. Redistributions of source code must retain the above copyright
###    notice, this list of conditions and the following disclaimer.
### 2. Redistributions in binary form must reproduce the above copyright
###    notice, this list of conditions and the following disclaimer in the
###    documentation and/or other materials provided with the distribution.

### THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
### IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
### OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
### IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
### INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
### NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
### DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
### THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
### (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
### THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import os
import re
import subprocess
from gettext import gettext as _

# ignored, new, normal, ignored changes,
# error, placeholder, vc added
# vc modified, vc conflict, vc removed
# locally removed, end
STATE_IGNORED, STATE_NONE, STATE_NORMAL, STATE_NOCHANGE, \
STATE_ERROR, STATE_EMPTY, STATE_NEW, \
STATE_MODIFIED, STATE_CONFLICT, STATE_REMOVED, \
STATE_MISSING, STATE_MAX = range(12)

class Entry(object):
    # These are the possible states of files. Be sure to get the colons correct.
    states = _("Ignored:Unversioned:::Error::Newly added:Modified:Conflict:Removed:Missing").split(":")
    states[STATE_CONFLICT] = "<b>%s</b>" % states[STATE_CONFLICT]
    assert len(states)==STATE_MAX

    def __init__(self, path, name, state):
        self.path = path
        self.state = state
        self.parent, self.name = os.path.split(path.rstrip("/"))
    def __str__(self):
        return "<%s:%s %s>\n" % (self.__class__, self.name, (self.path, self.state))
    def __repr__(self):
        return "%s %s\n" % (self.name, (self.path, self.state))
    def get_status(self):
        return self.states[self.state]

class Dir(Entry):
    def __init__(self, path, name, state):
        Entry.__init__(self, path, name, state)
        self.isdir = 1
        self.rev = ""
        self.tag = ""
        self.options = ""

class File(Entry):
    def __init__(self, path, name, state, rev="", tag="", options=""):
        assert path[-1] != "/"
        Entry.__init__(self, path, name, state)
        self.isdir = 0
        self.rev = rev
        self.tag = tag
        self.options = options

class Vc(object):

    PATCH_STRIP_NUM = 0
    PATCH_INDEX_RE = ''
    VC_DIR = None
    VC_ROOT_WALK = True
    VC_METADATA = None

    def __init__(self, location):
        # Save the requested diff directory/file.  It may be a sub-directory
        # of the repository we are diffing and can be useful in limiting meld's
        # output to the requested location.  It can also be used to determine
        # if the user is requesting a single-file diff or a diretcory diff.
        self.location = location

        if not os.path.isdir(location):
            path = os.path.dirname(self.location)
        else:
            path = location
        if self.VC_ROOT_WALK:
            self.root = self.find_repo_root(path)
        else:
            self.root = self.check_repo_root(path)

    def commit_command(self, message):
        raise NotImplementedError()
    def diff_command(self):
        raise NotImplementedError()
    def update_command(self):
        raise NotImplementedError()
    def add_command(self, binary=0):
        raise NotImplementedError()
    def remove_command(self, force=0):
        raise NotImplementedError()
    def revert_command(self):
        raise NotImplementedError()
    def resolved_command(self):
        raise NotImplementedError()
    def patch_command(self, workdir):
        return ["patch", "-p%i" % self.PATCH_STRIP_NUM, "-R", "-d", workdir]

    def check_repo_root(self, location):
        if not os.path.isdir(os.path.join(location, self.VC_DIR)):
            raise ValueError
        return location

    def find_repo_root(self, location):
        while True:
            try:
                return self.check_repo_root(location)
            except ValueError:
                pass
            tmp = os.path.dirname(location)
            if tmp == location:
                break
            location = tmp
        raise ValueError()

    def get_working_directory(self, workdir):
        return workdir

    def cache_inventory(self, topdir):
        pass

    def uncache_inventory(self):
        pass

    def update_file_state(self, path):
        """ Update the state of a specific file.  For example after a file
        has been modified and saved, its state may be out of date and require
        updating.  This can be implemented for Vc plugins that cache file
        states, eg 'git' an 'bzr' so that the top-level file status is always
        accurate.
        """
        pass

    def get_patch_files(self, patch):
        regex = re.compile(self.PATCH_INDEX_RE, re.M)
        return [f.strip() for f in regex.findall(patch)]

    def listdir_filter(self, entries):
        return [f for f in entries if f != self.VC_DIR]

    # Determine if a directory is a valid git/svn/hg/cvs/etc repository
    def valid_repo(self):
        return True

    def listdir(self, start):
        if start=="": start="."
        cfiles = []
        cdirs = []
        try:
            entries = os.listdir(start)
            entries.sort()
        except OSError:
            entries = []
        for f in self.listdir_filter(entries):
            fname = os.path.join(start, f)
            lname = fname
            if os.path.isdir(fname):
                cdirs.append( (f, lname) )
            else:
                cfiles.append( (f, lname) )
        dirs, files = self.lookup_files(cdirs, cfiles)
        return dirs+files

    def lookup_files(self, dirs, files):
        "Assume all files are in the same dir, files is an array of (name, path) tuples."
        directory = self._get_directoryname(files, dirs)
        if directory is None:
            return [], []
        else:
            return self._get_dirsandfiles(directory, dirs, files)

    def _get_directoryname(self, dirs, files):
        directory = None
        if len(files):
            directory = os.path.dirname(files[0][1])
        elif len(dirs):
            directory = os.path.dirname(dirs[0][1])
        return directory

    def _get_dirsandfiles(self, directory, dirs, files):
        raise NotImplementedError()


class CachedVc(Vc):

    def __init__(self, location):
        super(CachedVc, self).__init__(location)
        self._tree_cache = None

    def cache_inventory(self, directory):
        self._tree_cache = self._lookup_tree_cache(directory)

    def uncache_inventory(self):
        self._tree_cache = None

    def _lookup_tree_cache(self, directory):
        raise NotImplementedError()

    def _get_tree_cache(self, directory):
        if self._tree_cache is None:
            self.cache_inventory(directory)
        return self._tree_cache

# Return the stdout output of a given command
def popen(cmd, cwd=None):
    return subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE).stdout

# Return the return value of a given command
def call(cmd, cwd=None):
    return subprocess.call(cmd, cwd=cwd, stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT)
