# Python parser for Subversion skels

import string, re
from types import *

def parse(s):
  if s[0] != '(' and s[-1] != ')':
    raise ValueError("Improperly bounded skel: '%s'" % s)
  wholeskel = s
  s = s[1:-1].lstrip()
  prev_accums = []
  accum = []
  while 1:
    if len(s) == 0:
      return accum
    if s[0] in string.digits:
      split_tuple = s.split(' ',1)
      count = int(split_tuple[0])
      if len(split_tuple) > 1:
        s = split_tuple[1]
      else:
        s = ""
      accum.append(s[:count])
      s = s[count:].lstrip()
      continue
    if s[0] in string.ascii_letters:
      i = 0
      while (s[i] not in ' ()'):
        i += 1
        if i == len(s):
          break
      accum.append(s[:i])
      s = s[i:].lstrip()
      continue
    if s[0] == '(':
      new_accum = []
      accum.append(new_accum)
      prev_accums.append(accum)
      accum = new_accum
      s = s[1:].lstrip()
      continue
    if s[0] == ')':
      accum = prev_accums.pop()
      s = s[1:].lstrip()
      continue
    if s[0] == ' ':
      s = str.lstrip(' ')
      continue
    raise ValueError("Unexpected contents in skel: '%s'\n'%s'" % (s, wholeskel))


_ok_implicit = re.compile(r'^[A-Za-z]([^\(\) \r\n\t\f]*)$')
def unparse(struc):
  accum = []
  for ent in struc:
    if type(ent) == StringType:
      if len(ent) > 0 and _ok_implicit.match(ent[0]):
        accum.append(ent)
      else:
        accum.append(str(len(ent)))
        accum.append(ent)
    else:
      accum.append(unparse(ent))
  return "("+" ".join(accum)+")"


class Rev:
  def __init__(self, skelstring="(revision null)"):
    sk = parse(skelstring)
    if len(sk) == 2 and sk[0] == "revision" and type(sk[1]) == StringType:
      self.txn = sk[1]
    else:
      raise ValueError("Invalid revision skel: %s" % skelstring)

  def unparse(self):
    return unparse( ("revision", self.txn) )


class Change:
  def __init__(self, skelstring="(change null null null 0  0 )"):
    sk = parse(skelstring)
    if len(sk) == 6 and sk[0] == "change" and type(sk[1]) == type(sk[2]) \
        == type(sk[3]) == type(sk[4]) == type(sk[5]) == StringType:
          self.path = sk[1]
          self.node = sk[2]
          self.kind = sk[3]
          self.textmod = sk[4]
          self.propmod = sk[5]
    else:
      raise ValueError("Invalid change skel: %s" % skelstring)

  def unparse(self):
    return unparse( ("change", self.path, self.node, self.kind,
                     self.textmod and "1" or "", self.propmod and "1" or "") )


class Copy:
  def __init__(self, skelstring="(copy null null null)"):
    sk = parse(skelstring)
    if len(sk) == 4 and sk[0] in ("copy", "soft-copy") and type(sk[1]) \
        == type(sk[2]) == type(sk[3]) == StringType:
          self.kind = sk[0]
          self.srcpath = sk[1]
          self.srctxn = sk[2]
          self.destnode = sk[3]
    else:
      raise ValueError("Invalid copy skel: %s" % skelstring)

  def unparse(self):
    return unparse( (self.kind, self.srcpath, self.srctxn, self.destnode) )


class Node:
  def __init__(self,skelstring="((file null null 1 0) null null)"):
    sk = parse(skelstring)
    if (len(sk) == 3 or (len(sk) == 4 and type(sk[3]) == StringType)) \
        and type(sk[0]) == ListType and type(sk[1]) == StringType \
        and type(sk[2]) == StringType and sk[0][0] in ("file", "dir") \
        and type(sk[0][1]) == type(sk[0][2]) == type(sk[0][3]) == StringType:
          self.kind = sk[0][0]
          self.createpath = sk[0][1]
          self.prednode = sk[0][2]
          self.predcount = int(sk[0][3])
          self.proprep = sk[1]
          self.datarep = sk[2]
          if len(sk) > 3:
            self.editrep = sk[3]
          else:
            self.editrep = None
    else:
      raise ValueError("Invalid node skel: %s" % skelstring)

  def unparse(self):
    structure = [ (self.kind, self.createpath, self.prednode,
        str(self.predcount)), self.proprep, self.datarep ]
    if self.editrep:
      structure.append(self.editrep)
    return unparse( structure )


class Txn:
  def __init__(self,skelstring="(transaction null null () ())"):
    sk = parse(skelstring)
    if len(sk) == 5 and sk[0] in ("transaction", "committed", "dead") \
        and type(sk[1]) == type(sk[2]) == StringType \
        and type(sk[3]) == type(sk[4]) == ListType and len(sk[3]) % 2 == 0:
          self.kind = sk[0]
          self.rootnode = sk[1]
          if self.kind == "committed":
            self.rev = sk[2]
          else:
            self.basenode = sk[2]
          self.proplist = sk[3]
          self.copies = sk[4]
    else:
      raise ValueError("Invalid transaction skel: %s" % skelstring)

  def unparse(self):
    if self.kind == "committed":
      base_item = self.rev
    else:
      base_item = self.basenode
    return unparse( (self.kind, self.rootnode, base_item, self.proplist,
      self.copies) )


class SvnDiffWindow:
  def __init__(self, skelstructure):
    self.offset = skelstructure[0]
    self.svndiffver = skelstructure[1][0][1]
    self.str = skelstructure[1][0][2]
    self.size = skelstructure[1][1]
    self.vs_rep = skelstructure[1][2]

  def _unparse_structure(self):
    return ([ self.offset, [ [ 'svndiff', self.svndiffver, self.str ],
        self.size, self.vs_rep ] ])


class Rep:
  def __init__(self, skelstring="((fulltext 0  (md5 16 \0\0\0\0\0\0\0\0" \
          "\0\0\0\0\0\0\0\0)) null)"):
    sk = parse(skelstring)
    if type(sk[0]) == ListType and len(sk[0]) == 3 \
        and type(sk[0][1]) == StringType \
        and type(sk[0][2]) == ListType and len(sk[0][2]) == 2 \
        and type(sk[0][2][0]) == type(sk[0][2][1]) == StringType:
          self.kind = sk[0][0]
          self.txn = sk[0][1]
          self.cksumtype = sk[0][2][0]
          self.cksum = sk[0][2][1]
          if len(sk) == 2 and sk[0][0] == "fulltext":
            self.str = sk[1]
          elif len(sk) >= 2 and sk[0][0] == "delta":
            self.windows = list(map(SvnDiffWindow, sk[1:]))
    else:
      raise ValueError("Invalid representation skel: %s" % repr(skelstring))

  def unparse(self):
    structure = [ [self.kind, self.txn, [self.cksumtype, self.cksum] ] ]
    if self.kind == "fulltext":
      structure.append(self.str)
    elif self.kind == "delta":
      for w in self.windows:
        structure.append(w._unparse_structure())
    return unparse( structure )

