from __future__ import with_statement

import os, tempfile, threading, atexit, shutil

import pysvn

from bazbase import config
from bazbase.model import Element
from bazbase.db import TransactionAborted

from .share import svndriven
from . import custom, format

state = threading.local()
checkouts_lock = threading.Lock()
checkouts = set()

def _mkdir_p(path):
    bits = path.split('/')
    pathpnt = ''
    for b in bits:
        pathpnt += b+'/'
        if not os.path.exists(pathpnt):
            state.client.mkdir(pathpnt,"")

# helper func
def _setaddfile(path,val):
    #print path
    dir,fil = path.rsplit('/',1)
    _mkdir_p(dir)
    with open(path,'w') as fil:
        fil.write(val+'\n')
    try:
        state.client.add(path)
    except pysvn.ClientError:
        pass

class InvalidState(Exception):
    pass

class LockFailed(Exception):
    pass

@atexit.register
def cleanup_checkouts():
    with checkouts_lock:
        for c in checkouts:
            shutil.rmtree(c,ignore_errors=True)

def _elementpath(element,parent=None,symlink=False,orgmode=None):
    above = []
    if orgmode is None:
        orgmode = element.orgmode
    while True:
        if orgmode == 'toplevel':
            if symlink:
                above = [element.ename+'.top']
                symlink = False
            else:
                if len(above) > 0:
                    return os.path.join(*[element.ename]+above)
                else:
                    return os.path.join(element.ename,element.ename+'.def')
        elif orgmode == 'list':
            above = [element.ename+'.list']
        elif len(above) == 0:
            if element.hasChildren():
                above = [element.ename,element.ename+'.def']
            else:
                above = [element.ename+'.elm']
        else:
            above = [element.ename] + above
        if parent is not None:
            element = parent
            parent = None
        else:
            element = element.supertype
        if element is None:
            break
        elif element is False:
            return None
        orgmode = element.orgmode
    return os.path.join(*[config.get_global(u'appname')+'.meta']+above)

def _get_exclusive_global_lock():
    try:
        os.rmdir(os.path.join(custom.REPOSITORY,'bazsvn_access'))
    except OSError:
        raise LockFailed("Another bazsvn already has a global lock!")
    else:
        state.lock_state = True

def _get_shared_global_lock():
    if not hasattr(state,'hostname'):
        state.hostname = os.uname()[1]
    if not hasattr(state,'username'):
        state.username = custom.get_username()
    identifier = "%s-%d-%s" % (state.hostname, os.getpid(), state.username)
    try:
        os.mkdir(os.path.join(custom.REPOSITORY,'bazsvn_access',identifier))
    except OSError:
        raise LockFailed("Another bazsvn already has an exclusive lock!")
    else:
        state.lock_state = identifier

def _remove_global_lock(lock_state=None):
    if lock_state is None:
        lock_state = state.lock_state
    if lock_state == True:
        os.mkdir(os.path.join(custom.REPOSITORY,'bazsvn_access'))
    elif lock_state == False:
        pass
    else:
        os.rmdir(os.path.join(custom.REPOSITORY,'bazsvn_access',lock_state))

def notify(dct):
    
    if pysvn.wc_notify_state.conflicted in (dct['content_state'],
                                            dct['prop_state']):
        state.conflict = True

def begin():
    username = custom.get_username()
    if username:
        username = username.encode('utf-8')
    else:
        username = 'nobody'
    state.client = pysvn.Client()
    state.client.callback_notify = notify
    state.client.set_default_username('baz:%s'%(username))

    state.deltas = {}
    state.lock_state = False
    state.hostname = os.uname()[1]
    state.username = username
    state.conflict = False
                                             
    try:
        with checkouts_lock:
            state.tempd = checkouts.pop()
    except KeyError:
        state.tempd = tempfile.mkdtemp('.bazsvnco')
        state.checkout_thread = threading.Thread(
            target=lambda cl,tempd:cl.checkout('file:///'+os.path.abspath(custom.REPOSITORY),tempd),args=(state.client,state.tempd))
    else:
        state.checkout_thread = threading.Thread(
            target=lambda cl,tempd:cl.update(tempd),
            args=(state.client,state.tempd))
    state.checkout_thread.start()

def setprop(element,pname,val):
    path = _elementpath(element)
    state.deltas.setdefault(path,{}).setdefault(element.ename,{})[pname]=val

def delete(element,pname):
    path = _elementpath(element)
    state.deltas.setdefault(path,{}).setdefault(element.ename,{})[pname]=None

def esetattr(element,attr,val,bootstrapping=False):
    if attr == 'parent':
        assert element is not None
        assert isinstance(val,Element) or val is None,val.__class__
        olddelts = {}
        if element.supertype == val and not bootstrapping:
            return
        if element.supertype is not False:
            oldpath = _elementpath(element)
            if (oldpath in state.deltas
                and element.ename in state.deltas[oldpath]):
                olddelts = state.deltas[oldpath][element.ename]
        newpath = _elementpath(element,parent=val)
        if newpath.endswith('.list') and (element.supertype is False
                                          or not oldpath.endswith('.list')):
            if element.supertype is not False:
                edelete(element)
            state.deltas.setdefault(newpath,{}).setdefault('parentage',{}).setdefault(val,{})[element.ename] = olddelts
        elif (element.supertype is not False
              and oldpath.endswith('.list')
              and not newpath.endswith('.list')):
            do_something()
        elif (element.supertype is not False
              and oldpath.endswith('.list') and newpath.endswith('.list')):
            do_something_else()
        else:
            # Just a simple listless move
            assert newpath not in state.deltas,(element,newpath,state.deltas[newpath])
            if val is not None and not val.hasChildren() and val.orgmode != 'toplevel':
                # Transform from foo.elm to foo/foo.def
                oldparpath = os.path.dirname(newpath)+'.elm'
                newparpath = os.path.dirname(newpath)+'/'+val.ename +'.def'
                state.checkout_thread.join()
                state.client.mkdir(state.tempd+'/'
                                   +os.path.dirname(newpath),"")
                state.client.move(state.tempd+'/'+oldparpath,
                                  state.tempd+'/'+newparpath,force=True)
                assert newparpath not in state.deltas
                if oldparpath in state.deltas:
                    state.deltas[newparpath] = state.deltas[oldparpath]
                    del state.deltas[oldparpath]
            if element.supertype is False or bootstrapping:
                state.checkout_thread.join()
                _setaddfile(state.tempd+'/'+newpath,'')
            elif oldpath != newpath:
                state.checkout_thread.join()
                if oldpath.endswith('.def'):
                    assert newpath.endswith('.def')
                    oldpath = os.path.dirname(oldpath)
                    newpath = os.path.dirname(newpath)
                if element.supertype.countDescendants() == 1:
                    # We're the last child
                    oldparpath = os.path.join(os.path.dirname(path),
                                              element.supertype.ename+'.def')
                    newparpath = os.path.join(os.path.dirname(
                        os.path.dirname(path)),
                                              element.supertype.ename+'.elm')
                    state.client.move(oldparpath, newparpath, force=True)
                    state.client.remove([os.path.dirname(oldparpath)])
                
                state.client.move(state.tempd+'/'+oldpath,
                                  state.tempd+'/'+newpath,force=True)
            # Make a symlink for toplevel elements
            if element.orgmode == 'toplevel':
                state.checkout_thread.join()
                if element.supertype != val:
                    oldsymlink = _elementpath(element,symlink=True)
                    state.client.remove(state.tempd+'/'+oldsymlink)
                newsymlink = _elementpath(element,parent=val,symlink=True)
                if os.path.exists(state.tempd+'/'+newsymlink):
                    os.unlink(state.tempd+'/'+newsymlink)
                    dontadd=True
                else:
                    dontadd=False
                os.symlink('../'*newsymlink.count('/')+newpath,
                           state.tempd+'/'+newsymlink)
                if not dontadd:
                    state.client.add(state.tempd+'/'+newsymlink)
    elif attr == 'ename':
        assert False,"esetattr.ename not implemented"
        fil1 = _getepath(ename)
        fil2 = os.path.join(os.path.dirname(fil1),val)
        state.client.move(fil1,fil2,force=True)
    elif attr == 'orgmode':
        was = _elementpath(element)
        will = _elementpath(element,orgmode=val)
        if was != will:
            assert will not in state.deltas
            if was in state.deltas:
                state.deltas[will] = state.deltas[was]
                del state.deltas[was]
            state.checkout_thread.join()
            if element.orgmode == 'list':
                assert False
            elif val == 'list':
                assert False
            elif val == 'toplevel':
                assert element.orgmode == 'normal'
                if was.endswith('.def'):
                    # Move to toplevel and make symlink
                    state.client.move(state.tempd+'/'+os.path.dirname(was),
                                      state.tempd+'/'+os.path.dirname(will),
                                      force=True)
                    for k in state.deltas.keys():
                        if k.startswith(os.path.dirname(was)+'/'):
                            new = k[len(os.path.dirname(was)):]
                            new = os.path.dirname(will)+new
                            assert new not in state.deltas
                            state.deltas[new] = state.deltas[k]
                            del state.deltas[k]
                else:
                    assert was.endswith('.elm')
                    # Make dir, move, make symlink
                    state.client.mkdir(state.tempd+'/'+os.path.dirname(will),
                                       "")
                    state.client.move(state.tempd+'/'+was,
                                      state.tempd+'/'+will,force=True)
                # Symlink
                sympath = _elementpath(element,orgmode=val,symlink=True)
                os.symlink('../'*sympath.count('/')+os.path.dirname(will),
                           state.tempd+'/'+sympath)
                state.client.add(state.tempd+'/'+sympath)
            elif val == 'normal':
                assert element.orgmode == 'toplevel'
                if will.endswith('.def'):
                    # Move from toplevel and nix symlink
                    state.client.move(state.tempd+'/'+os.path.dirname(was),
                                      state.tempd+'/'+os.path.dirname(will),
                                      force=True)
                    for k in state.deltas.keys():
                        if k.startswith(os.path.dirname(was)+'/'):
                            new = k[len(os.path.dirname(was)):]
                            new = os.path.dirname(will)+new
                            assert new not in state.deltas
                            state.deltas[new] = state.deltas[k]
                            del state.deltas[k]
                else:
                    assert will.endswith('.elm')
                    # Move, rmdir, nix symlink
                    state.client.move(state.tempd+'/'+was,
                                      state.tempd+'/'+will,force=True)
                    state.client.remove([state.tempd+'/'+os.path.dirname(was)])
                # Nix symlink
                sympath = _elementpath(element,symlink=True)
                state.client.remove(state.tempd+'/'+sympath)
            else:
                raise InvalidState("Invalid orgmode '%s'!" % val)
    else:
        raise Exception('Unknown eattr %s!'%attr)

def edelete(element):
    rpath = _elementpath(element)
    state.checkout_thread.join()
    path = os.path.join(state.tempd,rpath)
    if path.endswith('.elm'):
        state.client.remove([path],
                            force=True)
        if element.supertype.countDescendants() == 1:
            # We're the last child
            oldparpath = os.path.join(os.path.dirname(path),
                                      element.supertype.ename+'.def')
            newparpath = os.path.join(os.path.dirname(os.path.dirname(path)),
                                      element.supertype.ename+'.elm')
            state.client.move(oldparpath, newparpath, force=True)
            state.client.remove([os.path.dirname(oldparpath)])
    elif path.endswith('.def'):
        raise InvalidState("Removing a node with children!")
    elif path.endswith('.list'):
        if os.path.basename(path) == element.ename+'.list':
            assert not element.hasChildren()
            state.client.remove([path],force=True)
        else:
            state.deltas.setdefault(path,{})[element.ename] = None
            if rpath in state.deltas and element.ename in state.deltas[rpath]:
                del state.deltas[rpath][element.ename]
            return # skip deltanukage
    else:
        assert False,path
    if rpath in state.deltas:
        del state.deltas[rpath]

def psetattr(prop,attr,val):
    assert attr in ('flavor','default','comment','pname')
    state.checkout_thread.join()
    if attr == 'pname':
        fil1 = '%s.meta/props/%s.prop' % (config.get_global(u'appname'),
                                     prop.name)
        fil2 = '%s.meta/props/%s.prop' % (config.get_global(u'appname'),
                                     val)
        assert fil2 not in state.deltas
        if os.path.exists(state.tempd+'/'+fil1):
            state.client.move(state.tempd+'/'+fil1,
                              state.tempd+'/'+fil2,force=True)
            if fil1 in state.deltas:
                state.deltas[fil2] = state.deltas[fil1]
                del state.deltas[fil1]
        else:
            _setaddfile(os.path.join(state.tempd,path),'')
    else:
        path = '%s.meta/props/%s.prop' % (config.get_global(u'appname'),
                                     prop.name)
        state.deltas.setdefault(path,{}).setdefault(prop.name,{})[attr] = val
        if not os.path.exists(state.tempd+'/'+path):
            _setaddfile(os.path.join(state.tempd,path),'')

def pdelete(prop):
    state.checkout_thread.join()
    fil = '%s.meta/props/%s.prop' % (state.tempd,config.get_global(u'appname'),
                                   prop.name)
    state.client.remove([state.tempd+'/'+fil],
                        force=True)
    if fil in state.deltas:
        del state.deltas[fil]

class parse_handler(object):
    def __init__(self,subs,path):
        self.subs = subs
        self.dir = state.tempd+'/'+os.path.dirname(path)
        self.noprop = False
    def prop(self,e,p,contents):
        assert not self.noprop
        if e in self.subs and p in self.subs[e]:
            ret = self.subs[e][p]
            del self.subs[e][p]
            return ret
        else:
            return True
    def start_element(self,e,parent=None):
        if e in self.subs and self.subs[e] is None:
            return False
        else:
            return True
    def get_new_properties(self,e):
        self.noprop = True
        return dict((p,self.subs[e][p]) for p in self.subs[e])
    def get_new_children(self,e):
        if e in self.parentage:
            return self.parentage[e]
        else:
            return []
    def set_extern(self,name,val):
        path = self.dir+'/'+name
        if val is not None:
            _setaddfile(path,val)
        else:
            if os.path.exists(path):
                state.client.remove([path],force=True)

def prepare():
    assert not svndriven()
    state.checkout_thread.join()
    try:
        # Do the runaround of state.deltas
        for path in state.deltas:
            inf = open(state.tempd+'/'+path)
            # We replace the old file with a new file with the same path,
            # for ease of processing
            os.unlink(state.tempd+'/'+path)
            outf = open(state.tempd+'/'+path,mode='w')
            elms = state.deltas[path]
            if (path.endswith('.elm') or path.endswith('.def')
                or path.endswith('.prop')):
                assert len(elms) == 1
            format.parse(state.tempd+'/'+path,
                         parse_handler(state.deltas[path],path),inf,outf)
            inf.close()
            outf.close()
        _get_exclusive_global_lock()
        #update here to make sure no transactions are in progress and to merge
        #harmless changes
        state.client.update(state.tempd)
        if state.conflict:
            raise TransactionAborted("Subversion conflict!")
    except:
        _remove_global_lock()
        state.revert_thread = threading.Thread(target=do_revert,
                                               args=(state.client,state.tempd))
        state.revert_thread.start()
        raise

def do_checkin(client,tempd,message,lock_state):
    try:
        # commit changes
        client.checkin(tempd, log_message=message)
    finally:
        _remove_global_lock(lock_state)
    with checkouts_lock:
        checkouts.add(tempd)    

def commit():
    assert not svndriven()
    if False:
        state.commit_thread = threading.Thread(
            target=do_checkin, args=(state.client,state.tempd,
                                     "Baz change by %s."%state.username,
                                     state.lock_state))
        state.commit_thread.start()
    else:
        do_checkin(state.client,state.tempd,
                   "Baz change by %s."%state.username,
                   state.lock_state)

def do_revert(client,tempd):
    client.revert(tempd, recurse=True)
    with checkouts_lock:
        checkouts.add(tempd)

def abort():
    assert not svndriven()
    _remove_global_lock()
    state.checkout_thread.join()
    state.revert_thread = threading.Thread(target=do_revert,
                                           args=(state.client,state.tempd))
    state.revert_thread.start()
