"""

Implementation notes:

For 1.1 I've introduced support for multiline rules,
and I think this makes the grammar non-LALR(1). Therefore,
additonal parsing step is needed.

After lexing, a hand-written parser splits input stream
into separate rule definitions, which are then fed to the LALR parser.

I'm clearly not an expert when it comes to LALR, so if anyone
knows how to do it with LALR(1) in an elegant way, drop me a line.
"""

from ply.lex import lex, LexToken
from ply.yacc import yacc

from nodes import Rule, Term, OptionalTerm, NamedTerm, Terms, OrTerm


t_ID = r'[a-zA-Z_][a-zA-Z0-9_]*'
t_COLON = r':'

t_PAREN_BEGIN = r'\('
t_PAREN_END = r'\)'
t_CURLY_BEGIN = r'{'
t_CURLY_END = r'}'
t_QUESTION = r'\?'
t_VBAR = r'\|'
t_RULE_HEADER = r'blablabla this token is autogenerated by split_rules'
t_NAMED_TERM_BEGIN = r'blablabla this token is autogenerated by split_rules'

def t_COMMENT(t):
  r'\#.*'
  return None

def t_error(a):
  raise SyntaxError("ERROR: Unexpected '%s'" % a.value[0])

t_ignore = ' \t\n'

tokens = []
name = None
tokens = [name[2:] for name in globals() if name.startswith('t_')]
tokens.remove('ignore')
tokens.remove('error')

start = 'rule'

def p_rule(p):
  "rule : RULE_HEADER or_terms"
  p[0] = Rule(p[1], p[2])

def p_or_terms(p):
  "or_terms : terms"
  p[0] = p[1]

def p_or_terms2(p):
  "or_terms : or_term"
  p[0] = p[1]

def p_terms(p):
  "terms : term"
  p[0] = Terms([p[1]])

def p_terms_append(p):
  "terms : terms term"
  p[0] = p[1]
  p[0].append(p[2])

def p_term(p):
  """
  term : optional_term
       | named_term
  """
  p[0] = p[1]

def p_term_literal(p):
  "term : ID"
  p[0] = Term(p[1])
  
def p_term_parens(p):
  "term : PAREN_BEGIN or_terms PAREN_END"
  p[0] = p[2]

def p_or_term(p):
  "or_term : terms VBAR terms"
  p[0] = OrTerm((p[1], p[3]))

def p_or_term_append(p):
  "or_term : or_term VBAR terms"
  p[0] = OrTerm(p[1].cases + (p[3], ))

def p_optional_term(p):
  "optional_term : term QUESTION"
  p[0] = OptionalTerm(p[1])

def p_named_term(p):
  "named_term : NAMED_TERM_BEGIN ID CURLY_END"
  p[0] = NamedTerm(p[1], p[2])

def p_named_term_default(p):
  "named_term : CURLY_BEGIN ID CURLY_END"
  p[0] = NamedTerm(p[2], p[2].lower())

def p_error(p):
  assert False, p

def split_rules(tokens):
  """
    Takes a sequence of tokens representing a ruleset and splits
    them into a list of token sequences, each representing a
    single rule (purpotedly). 

    Algorithm overview:
      1. We process the stream token by token, pushing them to an internal buffer.
      2. If the buffer ends with CURLY_BEGIN ID COLON, these tokens are replaced
         with a NAMED_TERM_BEGIN quasi-token.
      3. If the buffer ends with ID COLON, these tokens are replaced with
         a RULE_HEADER quasi-token, and the rest of the buffer is removed as
         a previous rule definition.
  """

  def clone_token(old_token, new_type):
    token = LexToken()
    token.type = new_type
    token.value = old_token.value
    token.lineno = old_token.lineno
    token.lexpos = old_token.lexpos
    return token
  
  def match_suffix(buffer, tokens):
    if len(buffer) < len(tokens):
      return False

    return all(buffer_token.type == target_token for buffer_token, target_token \
            in zip(buffer[-len(tokens):], tokens))

  NAMED_TERM_BEGIN = ('CURLY_BEGIN', 'ID', 'COLON')
  RULE_HEADER = ('ID', 'COLON')

  rules = []
  buffer = []
  for token in tokens:
    buffer.append(token)
    if match_suffix(buffer, NAMED_TERM_BEGIN):
      token = clone_token(buffer[-2], 'NAMED_TERM_BEGIN') # ID
      del buffer[-len(NAMED_TERM_BEGIN):]
      buffer.append(token)
    if match_suffix(buffer, RULE_HEADER):
      token = clone_token(buffer[-2], 'RULE_HEADER')
      del buffer[-len(RULE_HEADER):]
      if buffer:
        rules.append(buffer)
      buffer = [token]

  if buffer:
    rules.append(buffer)

  return rules

class TokenStream:
  "Wraps a token stream with a PLY-compatible lexer interface"

  def __init__(self, iterable):
    self.iterator = iter(iterable)

  def token(self):
    try:
      return next(self.iterator)
    except StopIteration:
      return None

lexer = lex()
parser = yacc(picklefile = 'easyply.tab') #FIXME: rename parsetab

def parse(text):
  lexer.input(text)
  rules = split_rules(lexer)
  return [parser.parse(lexer = TokenStream(rule)) for rule in rules]

