from __future__ import print_function
from __future__ import absolute_import
__all__ = ['eatAVee', 'eatAString',
           'parseKV', 'parseKVs',
           'parseASCIIReply',
           'parseRawReply']

""" Parsing utilities.

- Keywords can have zero or more comma-delimited values.
- The values are either quoted strings or unparsed tokens.
- Strings can be single- or double-quote delimited with internal quotes backslash-escaped. Non-escape
  backslashes within a string need to be backslash-escaped themselves.

- When parsed from a string, keywords are delimited by semicolons.

"""

import collections
import re

import CPL
from .Exceptions import ParseException

def eatAVee(s):
    """ Match a keyword value -- a possibly space-padded value ended by a whitespace, a comma, or a semicolon.

    Args:
       s - a string
       
    Returns:
      - the matched value. None if s is just whitespace.
      - any unmatched input, including the terminating character.
    """
    
    # CPL.log('eatAVee', 'called with %s' % (s))

    s = s.lstrip()
    if len(s) == 0:
        return '', ""

    # BEWARE - accept an empty value. 
    if s[0] == ';':
        return '', s

    # String parsing is trickier, let eatAString() handle that.
    if s[0] in "\"'":
        return eatAString(s)

    vEnd = len(s)
    for i,c in enumerate(s):
        if c in ';, \t\r\n\x0b\x0c':
            vEnd = i
            break

    if vEnd == 0:
        return '', s[vEnd:]
    
    return s[:vEnd], s[vEnd:]


def eatAString(s):
    """ Match a quote-escaped string. 

    Args:
      s - a string, which must begin with a singor- or double- quote.

    Returns:
      - the matched string, or the rest of the line. The quotes are NOT removed.
      - any unmatched input, including the terminating character.

    NOTE:
      If the end of the input string is hit before the leading quote is closed, a closing quote
      is (fairly) silently appended.
      
    """
    
    # CPL.log('eatAString', 'called with %s' % (s))

    if len(s) == 0:
        raise ParseException("unexpected empty string while parsing", leftoverText='')

    startQuote = s[0]
    if startQuote != "\"" and startQuote != "\'":
        raise ParseException("string does not start with a quote", leftoverText=s)

    escaping = False
    for i, c in enumerate(s):
        if i == 0:
            continue
        if escaping:
            escaping = False
            continue

        if c == startQuote:
            return s[:i+1], s[i+1:]
        if c == "\\":
            escaping = True

    # OK, we fell off the end of the string without matching the closing quote.
    # Force the string to look OK so that nobody else needs to deal with a mangled string.
    #
    CPL.log('eatAString', 'adding closing section (esc=%s) to string %r' % (escaping, s))

    if escaping:
        s = "%s\\%s" % (s, startQuote)
    else:
        s += startQuote

    return s, ''

# Match "  key = STUFF"
kv_re = re.compile(r"""
  ^\s*                          # Ignore leading space
  (?P<key>[a-z_][a-z0-9_-]*)    # Match keyword name
  \s*                           # Ignore spaces after keyname
  (?P<delimiter>[=;]|$)
  \s*
  (?P<rest>.*)                  # Match eveything after the delimiter""",
                  re.IGNORECASE|re.VERBOSE)

def parseKV(s):
    """ Try to parse a single KV.
    
    Return:
      { None, None, None } on end-of-input
      { K None rest-of-line } for a valueless keyword or
      { K V rest-of-line }
    """
    
    s = s.lstrip()
    if s == "":
        return None, None, None

    # Try to match for K=V. If we can't, try parsing as a valueless keyword.
    #
    match = kv_re.match(s)
    if match == None:
        raise ParseException(leftoverText=s)

    d = match.groupdict()
    K = d['key']
    rest = d['rest']

    # No equal sign? A valueless keyword.
    #
    if d['delimiter'] != '=':
        return K, None, rest

    # Build a list of values.
    #
    values = []
    while len(rest) != 0:

        # Parse a (sub-)value
        #
        try:
            V, rest = eatAVee(rest)
        except ParseException as e:
            e.prependText(rest)
            raise

        # BEWARE -- eatAVee() can return None for an empty value
        values.append(V)

        # Bail out if we:
        #   - hit EOL
        #
        rest = rest.lstrip()
        if len(rest) == 0:
            break
        
        # Keep gathering subvalues while we find commas.
        #
        if rest[0] == ',':
            rest = rest[1:]
        elif rest[0] == ';':
            rest = rest[1:]
            break
        else:
            break
        
    # Flatten singleton lists.
    if len(values) == 1:
        values = values[0]

    return K, values, rest

def parseKVs(s):
    """ Parse a string of key-value pairs into an OrderedDict .

    Returns:
      - an OrderedDict of keyword values.

    If a keyword has no value, the value is None
    Otherwise the value is a list of parsed values. Note that each value can be None.
    
    """
    
    KVs = collections.OrderedDict()
    rest = s

    while 1:
        try:
            key, values, rest = parseKV(rest)
        except ParseException as e:
            e.setKVs(KVs)
            raise
        
        if key == None:
            break

        # CPL.log('parseKVs', 'key=%r val=%r rest=%r' % (key, values, rest))
        KVs[key] = values

    return KVs

line_midcid_re = re.compile(r"""
  \s*                          # Skip leading whitespace
  (?P<mid>\d+)                 # integer MID
  \s+
  (?P<cid>[a-z0-9_][a-z0-9_.]*)                 # Integer CID. Should be more forgiving.
  \s+
  (?P<flag>[diwe:f>!])           # The flag. Should allow more characters, and check them elsewhere.
  (?P<rest>.*)""",
                     re.VERBOSE | re.IGNORECASE)

line_cidmid_re = re.compile(r"""
  \s*                          # Skip leading whitespace
  (?P<cid>[a-z0-9_][a-z0-9_.]*)  # Integer CID. Should be more forgiving.
  \s+
  (?P<mid>\d+)                 # integer MID
  \s+
  (?P<flag>[diwe:f>!])           # The flag. Should allow more characters, and check them elsewhere.
  (?P<rest>.*)""",
                     re.VERBOSE | re.IGNORECASE)

def parseASCIIReply(s, cidFirst=False):
    """ Try to parse a string into a dictionary containing:
         - mid   - the ICC's MID
         - cid   - the ICC's CID
         - flag  - the reply's flag character
         - KVs   - an OrderedDict of (key, value)s
    
        Returns that dictionary, or raises RuntimeError.

        If a reply line cannot be parsed at all, insert the entire line into the key 'RawLine'.
        If a reply line cannot be completely parsed, insert the unparsed section into the key 'UNPARSEDTEXT'.
    """

    if cidFirst:
        match = line_cidmid_re.match(s)
    else:
        match = line_midcid_re.match(s)
        
    if match == None:
        d = {}
        d['mid'] = 0
        d['cid'] = 0                    # or 'hub' or '.hub'?
        d['flag'] = 'w'
        d['RawText'] = s

        kvs = collections.OrderedDict()
        kvs['RawLine'] = [CPL.qstr(s)]
        d['KVs'] = kvs
        return d

    d = match.groupdict()

    try:
        KVs = parseKVs(d['rest'])
    except ParseException as e:
        KVs = e.KVs
        leftoverText = e.leftoverText

        # In this case, quote the offending text.
        KVs['UNPARSEDTEXT'] = [CPL.qstr(leftoverText)]
    except Exception as e:
        CPL.log("parseASCIIReply", "unexpected Exception: %s" % (e))
        KVs = collections.OrderedDict()
        KVs['UNPARSEDTEXT'] = [CPL.qstr(d['rest'])]
        
    d['KVs'] = KVs
    d['RawText'] = s
    del d['rest']
    
    return d

def parseRawReply(s, keyName="RawText"):
    """ Return a Reply with the entire input string saved in the given keyName keyword.

    Does not make any effort to determine command success, termination, failure, etc.
    """

    d = {}
    d['mid'] = 0
    d['cid'] = 0 
    d['flag'] = 'i'

    CPL.log('parseRawReply', 'consumed :%r:' % (s))
    
    kvs = collections.OrderedDict()
    kvs[keyName] = [CPL.qstr(s)]
    d['KVs'] = kvs
    d['RawText'] = s
    return d

def testParsing():
    OKtests = ("",
               "o",
               "expose",
               "expose boo",
               "expose boo=",
               "expose boo='bar'",
               "expose a b c=def h",
               "e 1 2 3",
               "msg let me 'abc def' xx=123 yy='oh let me be'")

    NGtests = ("'",
               "abc'=1",
               "shortString='abcd",
               "eol=2,3,")
    
    for t in OKtests:
        r = parseKVs(t)
        print("OKtest = %s" % (t))
        print("output = %s" % (r))
        print()
        
    for t in NGtests:
        print("NGtest = %s" % (t))
        try:
            r = parseKVs(t)
            print("output = %s" % (r))
        except Exception as e:
            print("exception = %s" % (e))
            
        print()

def testMatching():
    pass

if __name__ == "__main__":
    testParsing()
    testMatching()
             
