from __future__ import with_statement

import os, sys, subprocess
from cStringIO import StringIO
import codecs

import pysvn

import bazsvn
from bazsvn import hook
from bazbase.flavors import FLAVORS
from bazbase import db, custom, structure
from bazyaml import format

from . import share

class InvalidCommit(Exception):
    pass

class format_handler(object):
    def __init__(self, cat, cwd, is_toplevel):
        self.cat = cat
        self.cwd = cwd
        self.is_toplevel = is_toplevel
        self.saw_parent = False
    def start_element(self,e,parent=None):
        if parent is not None:
            pare = structure.get_element(parent)
            elm = structure.get_element(e)
            if elm is None:
                pare.create_child(e)
            else:
                elm.set_parent(pare)
        self.present_props = set()
        
    def end_element(self, e):
        """Remove any props that weren't present."""
        elm = structure.get_element(e)
        for pname in elm.list_props():
            if pname not in self.present_props:
                elm.remove_prop(pname)
        
    def prop(self, e, p, contents):
        if p == u'parent':
            if not self.is_toplevel:
                raise InvalidCommit(
                    "'parent' specified in non-toplevel element '%s'!" % e)
            # This is a toplevel element.
            parent = structure.get_element(contents)
            elm = structure.get_element(e)
            if elm is None:
                elm = parent.create_child(e)
            else:
                elm.set_parent(parent)
            elm.set_orgmode(u'toplevel')
            self.saw_parent = True
        else:
            if self.is_toplevel and not self.saw_parent:
                raise InvalidCommit("'parent' not specified at start of "
                                    "toplevel element %s/%s.yaml!" % (e, e))
            self.present_props.add(p)
            if isinstance(contents, format.Include):
                fmt = contents.ext
                contents = contents.val
            else:
                contents = contents.encode('utf-8')
                fmt = 'creole'
            structure.get_element(e).set_prop(p, contents, fmt)
    def get_include(self, path):
        try:
            return self.cat(os.path.join(self.cwd, path))
        except pysvn.ClientError:
            # This is generally a File Not Found
            return None

class prop_handler(object):
    def start_element(self,p,parent=None):
        assert parent is None
        prop = structure.get_prop(p)
        if prop is None:
            structure.create_prop(p)

    def end_element(self, p):
        pass
            
    def prop(self,p,a,contents):
        getattr(structure.get_prop(p), 'set_' + a)(
            format.interpret_as_prop(a, contents))

def make_svnlook_cat(repos):
    def cat(path):
        cmd = ['svnlook', 'cat', repos, path]
        p = subprocess.Popen(cmd,
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        out, err = p.communicate()
        if p.returncode != 0:
            raise EnvironmentError(p.returncode, err)
        return out
    return cat

def make_svnlook_tree(repos, transid=None):
    def tree(path, non_recursive=False):
        cmd = ['svnlook', 'tree', repos]
        if transid is not None:
            cmd += ['-t', transid]
        cmd += [path, '--full-paths']
        if non_recursive:
            cmd.append('--non-recursive')
        p = subprocess.Popen(cmd,
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        out, err = p.communicate()
        if p.returncode != 0:
            raise EnvironmentError(p.returncode, err)
        return (unicode(p, 'utf-8') for p in out.split())
    return tree

def prefixes(path):
    while '/' in path:
        path, chopped = path.rsplit('/', 1)
        print >>sys.stderr, path
        yield path

def precommit(repos, transid):
    # Handled by loading the config.
    #import bazsql
    #bazsql.activate()
    
    print >>sys.stderr,"Starting..."
    trans = pysvn.Transaction(repos, transid)

    username = trans.revpropget('svn:author')
    log_message = trans.revpropget('svn:log')

    if log_message.startswith('Web Edit'):
        # It was caused by our baz hook, so don't send it back
        return
    apply_changes(trans.changed(), username, cat=lambda f: trans.cat(f),
                  tree=make_svnlook_tree(repos, transid))

def apply_changes(orig_changes, username, cat, tree):
    bazsvn.custom.get_username = lambda: username

    share.make_svndriven()
    if hook == custom.version_control_hook:
        custom.version_control_hook = None

    # delfiles is a list in reverse depth-first order; it's deleted in order
    delfiles = []
    delprops = []
    needskids = set()
    needsnokids = set()

    with db.begin_transaction():
        changes = dict(orig_changes)
        # Recurse down added directories
        for fil in orig_changes:
            action,kind,text_mod,prop_mod = changes[fil]
            if kind == pysvn.node_kind.dir:
                if action == 'A':
                    for cont in tree(fil):
                        if cont.endswith('/'):
                            cont = cont[:-1]
                            ckind = pysvn.node_kind.dir
                        else:
                            ckind = pysvn.node_kind.file
                        changes[cont] = (action, ckind, True, False)
                elif action == 'D':
                    # Directory deletes are special
                    delparent = fil.rsplit('/', 1)[-1]
                    e = structure.get_element(delparent)
                    if e is not None:
                        parent_path = "%s/%s.yaml" % (fil, delparent)
                        if hook._elementpath(e) == parent_path:
                            # We deleted a whole tree.
                            for descendant in [e] + e.get_descendants():
                                changes[hook._elementpath(descendant)] = (
                                    action, pysvn.node_kind.file, True, False)
            elif kind == pysvn.node_kind.file and not fil.endswith('.yaml'):
                # Could be a changing include...
                parent = fil.rsplit('/', 1)[0]
                if parent not in changes or changes[parent][0] != 'D':
                    for neighbor in tree(parent, non_recursive=True):
                        if (neighbor.endswith('.yaml')
                            and neighbor not in changes):
                            changes[neighbor] = ('R', pysvn.node_kind.file,
                                                 False, False)
        
        keys = changes.keys()
        # We want added props, then fewer path segments first, then defs
        # followed by the rest alphabetically, followed by deleted props
        keys.sort()
        keys.sort(key=lambda s:s.count('/') < 1 or s.split('/')[-1] != s.split('/')[-2]+'.yaml')
        keys.sort(key=lambda s:s.count('/'))
        keys.sort(key=lambda s:not s.startswith('Object/'))
        keys.sort(key=lambda s:not s.startswith('props/'))
        print >>sys.stderr,"Keys: ",keys

        # Precalculate old paths, because after we've made chances some
        # of these could change.
        oldpaths = {}
        for ename in (k.rsplit('/', 1)[-1].rsplit('.yaml', 1)[0]
                      for k in keys
                      if (k.endswith('.yaml')
                          and not k.startswith('props/'))):
            e = structure.get_element(ename)
            if e is not None:
                oldpaths[ename] = hook._elementpath(e)
        
        while len(keys) > 0:
            fil = keys.pop(0)
            action,kind,text_mod,prop_mod = changes[fil]
            print >>sys.stderr, "Considering %s %s... (%s)" % (
                action, fil, kind)

            path = fil.split('/')
            is_def = len(path) >= 2 and path[-1] == path[-2]+'.yaml'
            is_toplevel = False

            if len(path) <= 0:
                assert action in ('A','R')
                continue

            if (path[-1].rsplit('.', 1)[-1] != 'yaml'
                or len(path) <= 1
                or ' ' in path[0]
                or '.' in path[0]):
                continue
            assert kind == pysvn.node_kind.file
            if (path[0] == 'props'):
                if len(path) != 2:
                    # prop files only matter in props/
                    continue
                pname = path[-1][:-5]
                cls = structure.Prop
            else:
                ename = path[-1].rsplit('.',1)[0]
                cls = structure.Element

            if action == 'D':
                if cls == structure.Prop:
                    delprops.append(pname)
                else:
                    if ename == u'Object':
                        raise InvalidCommit(
                            "You can't delete the root element.")
                    if len(path) >= 2 and not is_def:
                        parent = structure.get_element(path[-2])
                    elif len(path) >= 3 and is_def:
                        parent = structure.get_element(path[-3])
                    else:
                        assert is_def and len(path) == 2, (is_def, path)
                        parent = None
                    if parent is not None and parent.ename not in needsnokids:
                        needskids.add(parent.ename)
                    delfiles.insert(0, fil)
            else:
                assert action in ('A','R'),action
                if (cls == structure.Element and len(path) < 3
                    and is_def and ename != u'Object'):
                    is_toplevel = True
                if action == 'A' and cls == structure.Element:
                    if not is_def:
                        assert len(path) > 1
                        parname = path[-2]
                    else:
                        assert path[-2] == ename
                        if len(path) >= 3:
                            parname = path[-3]
                        else:
                            # Could be Object or another toplevel.
                            parname = None
                    if parname is not None:
                        parent = structure.get_element(parname)
                        if parent is None:
                            raise InvalidCommit(
                                "Parent '%s' for element '%s' undefined!"
                                % (parname, ename))
                    else:
                        parent = None
                    e = structure.get_element(ename)
                    if e is None:
                        if parent is not None:
                            parent.create_child(ename)
                        elif ename == u'Object':
                            structure.create_root_element(ename)
                    else:
                        # Make sure our old loc is deleted
                        oldpath = oldpaths[ename]
                        if oldpath in delfiles:
                            delfiles.remove(oldpath)
                        elif (oldpath in keys
                              and changes[oldpath][0] == 'D'):
                            keys.remove(oldpath)
                        elif fil != oldpath:
                            # If you're moving something to where it should
                            # have been all along, you're fixing something,
                            # so be quiet.  Otherwise, error.
                            raise InvalidCommit("Path '%s' added, but the exising location for this element, '%s', was not removed!" % (fil,oldpath))
                        if not is_toplevel:
                            e.set_orgmode(u'normal')
                            e.set_parent(parent)
                    if not is_def:
                        needsnokids.add(ename)
                        if ename in needskids:
                            needskids.remove(ename)
                    if (is_def and len(path) != 2):
                        needskids.add(ename)
                        if ename in needsnokids:
                            needsnokids.remove(ename)
                catio = codecs.getreader('utf-8')(StringIO(cat(fil)))
                # Now do the appropriate mods
                if cls == structure.Prop:
                    format.parse(fil,prop_handler(),inf=catio)
                else:
                    format.parse(fil, format_handler(cat,
                                                     os.path.dirname(fil),
                                                     is_toplevel),
                                 inf=catio)

        # Do cleanup
        for f in delfiles:
            ename = f.rsplit('/',1)[-1].rsplit('.',1)[0]
            print >>sys.stderr, "Deleting element %s." % ename
            e = structure.get_element(ename)
            db.edelete(e)
            if ename in needskids:
                needskids.remove(ename)
            if ename in needsnokids:
                needsnokids.remove(ename)
        for pname in delprops:
            db.pdelete(structure.get_prop(pname))
        for nk in needskids:
            if len(structure.get_children(nk)) <= 0:
                raise InvalidCommit("If %s has no kids, it needs to be in the form %s.yaml, not %s/%s.yaml!"%((nk,)*4))
        for nnk in needsnokids:
            if len(structure.get_children(nnk)) > 0:
                raise InvalidCommit("If %s has kids and is not toplevel, it needs to be in the form %s/%s.yaml, not %s.yaml!"%((nk,)*4))
        object = structure.get_element(u'Object')
        if object is None:
            raise InvalidCommit("No root element Object!")
