"""Implements the uvc command processing."""

import sys
import os
import subprocess
import tempfile
import logging
from cStringIO import StringIO

from uvc import commands, hg, svn
from uvc.util import run_in_directory
from uvc.path import path
from uvc.exc import *

log = logging.getLogger("uvc.main")

dialects = dict(hg=hg.HgDialect(), svn=svn.SVNDialect())

class IOWrapper(object):
    def __init__(self, file):
        self.file = file
        self.return_code = None
        
    def __getattr__(self, attr):
        return getattr(self.file, attr)

class Context(object):
    # all schemes are allowed
    remote_scheme_whitelist = None
    
    # a user can be set, because some DVCS operations
    # want a user to flag the commits, etc.
    user = None
    
    def __init__(self, working_dir, auth=None, timeout=-1,
                 output=None):
        """working_dir is the working directory in which commands should
        run. auth is a dictionary of authentication information:
        
        * type: 'ssh' or 'password'
        * key: for SSH, this should be the path to the user's private key
        * username: for SSH or password auth, this is the username to use
        * password: for password auth, this is the password to use
        
        timeout is the time (in seconds) that the command is allowed
        to run.
        
        output is a file-like object (specifically with a write method)
        to which the output is sent and return_code is set when
        the command is done. If it's not specified, it's set
        to sys.stdout.
        """
        self.working_dir = path(working_dir)
        self.auth = auth
        self.timeout = timeout
        self.output = IOWrapper(sys.stdout) if output is None else output
        
    def _normalize_path(self, unnorm_path):
        norm_path = self.working_dir / path(unnorm_path)
        norm_path = norm_path.normpath()
        norm_path = norm_path.realpath()
        norm_path = norm_path.abspath()
        return norm_path
    
    def validate_new_directory(self, newdir):
        """Confirms that newdir does not exist, raising a 
        FileError otherwise."""
        newdir = self._normalize_path(newdir)
        if newdir.exists():
            if newdir.isdir():
                raise FileError("Directory %s already exists" % newdir)
            else:
                raise FileError("Cannot create %s, path exists" % newdir)
        return newdir
    
    def validate_existing_directory(self, existingdir):
        """Confirms that existingdir does exist, raising a FileError
        otherwise."""
        existingdir = self._normalize_path(existingdir)
        if not existingdir.isdir():
            raise FileError("Directory %s does not exist" % existingdir)
        return existingdir
        
    def validate_new_file(self, newfile):
        """Confirms that newfile does not exist, raising a
        FileError otherwise."""
        newfile = self._normalize_path(newfile)
        if newfile.isfile():
            raise FileError("File %s already exists" % newfile)
        if newfile.isdir():
            raise FileError("%s exists as a directory" % newfile)
        return newfile
        
    def validate_existing_file(self, existingfile):
        """Confirms that existingfile exists, raising a FileError
        otherwise."""
        existingfile = self._normalize_path(existingfile)
        if not existingfile.isfile():
            raise FileError("File %s does not exist" % existingfile)
        return existingfile
        
    def validate_exists(self, existing):
        """Confirms that existing is a directory or
        file, raising a FileError otherwise."""
        existing = self._normalize_path(existing)
        if not existing.exists():
            raise FileError("File or directory %s does not exist" % (existing))
        return existing
        
    def validate_new_file_or_existing_dir(self, totest):
        """Confirms that totest either refers to a file that does
        not exist or a directory that does. This is useful for
        copy operations"""
        totest = self._normalize_path(totest)
        if not totest.exists():
            return totest
        if not totest.isdir():
            raise FileError("%s already exists" % totest)
        return totest
        
    def validate_path(self, path):
        """Validates that the path is legitimate. It doesn't
        need to point to anything existing, it just has to
        be able to pass _normalize_path."""
        return self._normalize_path(path)
    
class SecureContext(Context):
    # this is not the ideal place for this
    remote_scheme_whitelist = set(["http", "https", "ssh", "svn", "git", "bzr",
                                "svn+ssh"])
    
    def _normalize_path(self, unnorm_path):
        """Ensures that the path provided is underneath
        this context's working directory."""
        norm_path = super(SecureContext, self)._normalize_path(unnorm_path)
        rel_path = self.working_dir.relpathto(norm_path)
        if rel_path.isabs() or rel_path.startswith(".."):
            raise SecurityError("Path is outside of working directory: %s" 
                            % unnorm_path)
        return norm_path
    
def get_dialect(dialect_name):
    """Looks up the dialect in the dialect registry by name."""
    return dialects.get(dialect_name)

def infer_dialect(directory):
    d = set(dialects.values())
    cwd = os.getcwd()
    directory = os.path.abspath(directory)
    try:
        os.chdir(directory)
        remove_dialects = set()
        prev = None
        # stop when we hit the root directory (os.dirname
        # will stop giving us different values)
        while prev != directory:
            for dialect in d:
                is_match = dialect.cwd_is_this_dialect()
                if is_match == 0:
                    continue
                elif is_match == 1:
                    break
                else:
                    # in the case of Subversion, for example,
                    # if the directory doesn't have .svn in it, we
                    # know there's no match
                    remove_dialects.add(dialect)
            
            if is_match == 1:
                return dialect
            
            d.difference_update(remove_dialects)
            remove_dialects.clear()
            prev = directory
            directory = os.path.dirname(directory)
    finally:
        os.chdir(cwd)
    return None

def get_command_class(context, args, dialect=None):
    """This is similar to convert, but removes a step from the
    process. Use this if you need to inspect the command
    class before fulling setting up the context. Then,
    use from_args on the returned class to get an instance."""
    if dialect is None and args[0] in dialects:
        dialect_name = args.pop(0)
        dialect = get_dialect(dialect_name)
        command_name = args.pop(0)
        return dialect.get_dialect_command_class(command_name)
    else:
        generic_command_class = commands.get_command_class(context, args)
        guessed_dialect = generic_command_class.guess_dialect(context, args)
        if guessed_dialect:
            dialect = get_dialect(guessed_dialect)
        if dialect is None:
            raise commands.UVCError("Cannot run without knowing which VCS to use")
        return dialect.convert_class(generic_command_class)

def convert(context, args, dialect=None):
    """Converts the arguments provided into a command in the given dialect,
    or figures out the dialect for the project."""
    cmdclass = get_command_class(context, args, dialect)
    return cmdclass.from_args(context, args)

def run_command(command, context):
    command_line = command.get_command_line()
    
    # in some cases, such as Subversion's version of the
    # generic "commit", which is supposed to commit locally,
    # the command itself runs no vcs commands.
    if not command_line:
        return command.generate_output(context)
        
    log.debug("Running: %s", (command_line,))
    log.debug("Working dir: %s", context.working_dir)

    run_in_directory(context.working_dir, command_line, 
        timeout=context.timeout, 
        output=context.output)
    
    if context.output.return_code == 0 and \
        hasattr(command, "command_successful"):
        command.command_successful()
    
def _get_command_with_blanks_filled_in(context, args, dialect):
    """This will launch the user's text editor for
    items that really need to be filled in (such as
    commit messages)."""
    while True:
        try:
            return convert(context, args, dialect=dialect)
        except commands.GetValueFromEditor, e:
            editor = os.environ.get("EDITOR", "vi")
            tfile_handle, tfile_name = tempfile.mkstemp()
            try:
                command_line = [editor, tfile_name]
                p = subprocess.Popen(command_line).wait()
                contents = open(tfile_name).read()
            finally:
                os.unlink(tfile_name)
            if not contents:
                print "No text entered... exiting."
                sys.exit(1)
            args = e.template_args
            for i in range(0, len(args)):
                if args[i] == commands.inserted_value:
                    args[i] = contents
            
def is_new_project_command(args):
    log.debug("Verifying new project for %s", args)
    if args[0] == "clone" or args[0] == "checkout" \
       or (len(args) > 2 and " ".join(args[0:2]) in
       ["hg clone", "bzr clone", "git clone", "svn checkout"]):
        return True
    return False

def run(args=None):
    if args is None:
        args = sys.argv
    args.pop(0)
    
    cwd = os.getcwd()
    context = Context(cwd)
    
    if args and args[0] in dialects:
        dialect = None
    elif not is_new_project_command(args):
        dialect = infer_dialect(cwd)
    else:
        dialect = None
    
    command = _get_command_with_blanks_filled_in(context, args, dialect)
    run_command(command, context)

def call_uvc(directory, sub_command, params):
    """ Utility to call UVC from a given directory. This roughly the same as:
    $ cd $directory
    $ uvc $sub_command $params | $return
    'params' should be an array of params to pass into the sub_command.
    Returns an IOWrapper containing a StringIO with the output or containing the
    object passed in on the output_file parameter """
    output_file = StringIO()
    output = IOWrapper(output_file)
    context = SecureContext(directory, output=output)
    dialect = infer_dialect(directory);
    command_class = get_command_class(context, [ sub_command ], dialect)
    command = command_class.from_args(context, params)
    run_command(command, context)
    # TODO: This is a bit of a hack to allow call_uvc_data to get at the command
    output.command = command
    return output

def call_uvc_data(directory, sub_command, params):
    """ Utility to call UVC from a given directory. This roughly the same as:
    $ cd $directory
    $ uvc $sub_command $params | get_structured_data($return)
    'params' should be an array of params to pass into the sub_command.
    Returns output from command.get_structured_data rather than a string. """
    output = call_uvc(directory, sub_command, params)
    return output.command.get_structured_data(output.file.getvalue())
