#!/usr/bin/env python2

import os
import os.path
import sys
import re
import argparse
import StringIO
import codecs
import xml.etree.ElementTree as et

class SyntaxError(Exception):
  def __init__(self, args, input, msg):
    fn = args.input
    with open(fn) as f:
      s = f.read()
      pos = len(s) - len(input)
      line, column = 1, 1
      for i in xrange(max(0, pos-1)):
        column += 1
        if s[i] == '\n':
          line += 1
          column = 1
    super(SyntaxError, self).__init__("%s:%d:%d: %s" % (fn, line, column, msg))

TBLOCK_VERBATIM = r'^(<<<)(%?[\w][\w:.]*)[ \t]*'
TBLOCK = r'^(<)(%?[\w][\w:.]*)[ \t]*'
TCLOSE_VERBATIM = r'^>>>'
TCLOSE = r'^>'
TATTR = r"^[ \t]*([\w][\w\-:.]*)=(?:\'((?:[^']|\\')*)\'|(\S+))[ \t]*(?:\\\n)?"
TATTR_SINGLE = r"[ \t]*([\w\-:.]+)[ \t]*(?:\\\n)?"
TOPEN = r'^\|'
TEOL = r'^(?:[ \t\r]*\n)+( *)'
# Must be last.
TANY = r'^(.+?)(<|>|<<<|>>>|\n)'
TOKENS = [TBLOCK_VERBATIM, TBLOCK, TCLOSE_VERBATIM, TCLOSE, TOPEN, TEOL, TANY]
ATTRS = [TATTR, TATTR_SINGLE, TEOL]

def token(input, ignore_tokens=None):
  for t in TOKENS:
    m = re.match(t, input)
    if m is not None and (ignore_tokens is None or t not in ignore_tokens):
      return t, m
  return None, None

def attr(input):
  for t in ATTRS:
    m = re.match(t, input)
    if m is not None:
      return t, m
  return None, None

def advance(input, m, gr=0):
  if m is None:
    return input
  return input[len(m.group(gr)):]

def rewind(input, m, gr=0):
  return m.group(gr) + input

def strip_whitespaces(s):
  return s.lstrip(' \t').rstrip(' \t')

def strip_indent(indent, s):
  regex = ''.join([' ' for _ in range(indent)]) + '$'
  return re.sub(regex, '', s)

# When this is called, the tag corresponding to the directive must
# have been ended.
def try_process_directive(args, env, orig_builder, parent, el, orig_input, indent):
  is_directive = el.tag.startswith('%')
  if not is_directive:
    return orig_input, indent

  if parent is not None:
    parent.remove(el)
    builder = [et.TreeBuilder()]
  else:
    # No parent, the directive is at the root, we have to create a new
    # TreeBuilder to get rid of the node of the directive.
    orig_builder[0] = et.TreeBuilder()
    builder = orig_builder

  if el.tag == '%include':
    fn = el.get('path')
    if fn is None:
      raise Exception("Malformed directive '%include', missing required 'path' attribute")
    with codecs.open(fn, encoding='utf-8') as f:
      input = f.read()

    if fn.endswith('.nxml'):
      while True:
        input, ok = parse(args, env, builder, input)
        if not ok:
          break
        if parent is None:
          return orig_input, indent
        else:
          try:
            root = builder[0].close()
            parent.append(root)
          except AssertionError, ignore:
            # Empty builder
            pass
    else:
      # Not an nxml file, this contains just text.
      parent.text += input
  elif el.tag == '%set':
    for name, value in el.items():
      env[name] = value
  else:
    raise Exception("Unknown directive '%s'" % el.tag)

  return orig_input, indent

def block_start(indent, builder, input, tag):
  el = builder[0].start(tag, {})

  while True:
    tok, m = attr(input)
    if tok == TEOL and len(m.group(1)) != indent:
      break
    input = advance(input, m)
    if tok is None:
      break

    if tok == TATTR:
      el.set(m.group(1), m.group(2))
    else:
      el.set(m.group(1), '')

  return input, el

def block(args, env, indent, builder, input, parent):
  tok, m = token(input)
  if m is None or tok not in [TBLOCK, TBLOCK_VERBATIM]:
    return input, indent
  input = advance(input, m)

  chevrons = m.group(1)
  verbatim = len(chevrons) == 3
  if verbatim:
    closer = TCLOSE_VERBATIM
    non_closer = TCLOSE
    starter = [TBLOCK_VERBATIM]
    non_starter = [TBLOCK]
  else:
    closer = TCLOSE
    non_closer = TCLOSE_VERBATIM
    starter = [TBLOCK, TBLOCK_VERBATIM]
    non_starter = []

  tag = m.group(2)
  input, el = block_start(indent, builder, input, tag)

  tok, m = token(input)
  input = advance(input, m)

  need_explicit_closer = False
  if tok == TOPEN:
    need_explicit_closer = True
  elif tok == TEOL and len(m.group(1)) > indent:
    if len(m.group(1)) != indent + 2:
      raise SyntaxError(args, input,
          "Invalid indentation")
    # Equivalent to TOPEN.
    indent = len(m.group(1))
  elif tok == TEOL:
    raise SyntaxError(args, input,
        "Missing explicit closer '%s'" % closer)
  elif tok == closer:
    builder[0].end(tag)
    return try_process_directive(args, env, builder, parent, el, input, indent)
  else:
    if m is None:
      raise SyntaxError(args, input, "Unexpected end of file")
    else:
      raise SyntaxError(args, input,
          "Unexpected token '%s': '%s'" \
              % (tok, m.group(0)))

  ignore_tokens = set([TOPEN, non_closer] + non_starter)
  data = ''
  explicit_closer = False
  while True:
    tok, m = token(input, ignore_tokens=ignore_tokens)
    if tok is None:
      closing_indent = indent
      break
    input = advance(input, m)

    if tok in starter:
      # Sub block.
      builder[0].data(data)
      data = ''

      input = rewind(input, m)
      input, closing_indent = block(args, env, indent, builder, input, el)
      if closing_indent < indent:
        break

    elif tok == closer:
      explicit_closer = True
      closing_indent = indent
      break

    elif tok == TEOL and len(m.group(1)) < indent:
      if len(m.group(1)) % 2 != 0:
        raise SyntaxError(args, input,
            "Invalid indentation")
        data += strip_indent(indent, m.group(0))
      # Close.
      input = rewind(input, m)
      closing_indent = len(m.group(1))
      break

    elif tok == TEOL:
      data += strip_indent(indent, m.group(0))
      # Continue content.

    elif tok == TANY:
      # Content.
      input = rewind(input, m, 2)
      data += strip_indent(indent, m.group(1))

    else:
      raise SyntaxError(args, input,
          "Unexpected token '%s': '%s'" % (tok, m.group(0)))

  if not explicit_closer:
    if need_explicit_closer:
      raise SyntaxError(args, input,
          "Missing explicit closer '%s'" % closer)
    data = data.rstrip()
  builder[0].data(data)
  builder[0].end(tag)
  return try_process_directive(args, env, builder, parent, el, input, closing_indent)


re_leading_blanks = re.compile(r'^[\s\n]+', re.S)

def parse(args, env, builder, input):
  input = re_leading_blanks.sub('', input)
  if len(input) == 0:
    return input, False
  prev_len = len(input)

  input, _ = block(args, env, 0, builder, input, None)

  if len(input) == prev_len:
    return input, False
  else:
    return input, True

class XML(object):
  LANGS = ['xml', 'html5']

  @classmethod
  def process(cls, out_fn, env, elements):
    with codecs.open(out_fn, 'w') as out:
      lang = env.get('lang')
      if lang == 'xml':
        print>>out, '<?xml version="1.0"?>'
        method = 'xml'
      elif lang == 'html5':
        print>>out, '<!DOCTYPE html>'
        method = 'html'
      else:
        raise Exception("%s does not support lang='%s'" % (cls, lang))

      for element in elements:
        tree = et.ElementTree(element)

        buf = StringIO.StringIO()
        tree.write(buf, encoding='utf-8', method=method)
        s = buf.getvalue()
        buf.close()

        print>>out, s

PROCESSORS = [XML]

def find_processor(env):
  lang = env.get('lang')
  for p in PROCESSORS:
    if lang in p.LANGS:
      return p
  raise Exception("No valid processor found for lang='%s'" % lang)

def main():
  argparser = argparse.ArgumentParser()
  argparser.add_argument('input',
      help='stdin if -')
  argparser.add_argument('output', nargs='?', default='/dev/stdout',
      help='stdout if ommitted')
  argparser.add_argument('--set', action='append', metavar='name=value',
      help='environment override')

  args = argparser.parse_args()
  if args.input == '-':
    args.input = '/dev/stdin'

  with codecs.open(args.input, encoding='utf-8') as f:
    input = f.read()

  env = { 'lang': 'xml' }
  # 1-element array used as a reference: see try_process_directive()
  builder = [et.TreeBuilder()]
  elements = []

  while True:
    input, ok = parse(args, env, builder, input)
    if not ok:
      break
    try:
      root = builder[0].close()
      elements.append(root)
    except AssertionError, ignore:
      # Empty builder
      pass

  if args.set is not None:
    for pair in args.set:
      m = re.match(r"(\w+)='?(.*)'?", pair)
      if m is None:
        raise Exception("Malformed --set argument %s\n" % pair)
      env[m.group(1)] = m.group(2)

  processor = find_processor(env)
  processor.process(args.output, env, elements)

if __name__ == '__main__':
  main()
