# -*- test-case-name: nevow.test.test_json -*-
# Copyright (c) 2004-2007 Divmod.
# See LICENSE for details.

"""
JavaScript Object Notation.

This is not (nor does it intend to be) a faithful JSON implementation, but it
is kind of close.
"""

import re, types

from nevow.inevow import IAthenaTransportable
from nevow import rend, page, _flat, tags

class ParseError(ValueError):
    pass

whitespace = re.compile(
            r'('
            r'[\r\n\t\ ]+'
            r'|/\*.*?\*/'
            r'|//[^\n]*[\n]'
            r')'
            , re.VERBOSE + re.DOTALL)
openBrace = re.compile(r'{')
closeBrace = re.compile(r'}')
openSquare = re.compile(r'\[')
closeSquare = re.compile(r'\]')

class StringTokenizer(object):
    """
    because r'(?<!\\)"([^"]+|\\")*(?<!\\)"'
    """

    def match(self, s):
        if not s.startswith('"'):
            return None

        bits = []

        SLASH = "\\"

        IT = iter(s)
        bits = [IT.next()]
        for char in IT:
            bits.append(char)
            if char == SLASH:
                try:
                    bits.append(IT.next())
                except StopIteration:
                    return None
            if char == '"':
                self.matched = ''.join(bits)
                return self

        return None

    def group(self, num):
        return self.matched

string = StringTokenizer()
identifier = re.compile(r'[A-Za-z_][A-Za-z_0-9]*')
colon = re.compile(r':')
comma = re.compile(r',')
true = re.compile(r'true')
false = re.compile(r'false')
null = re.compile(r'null')
undefined = re.compile(r'undefined')
floatNumber = re.compile(r'-?([1-9][0-9]*|0)(\.[0-9]+)([eE][-+]?[0-9]+)?')
longNumber = re.compile(r'-?([1-9][0-9]*|0)([eE][-+]?[0-9]+)?')

class StringToken(str):
    pass

class IdentifierToken(str):
    pass

class WhitespaceToken(object):
    pass

def jsonlong(s):
    if 'e' in s:
        m, e = map(long, s.split('e', 1))
    else:
        m, e = long(s), 0
    return m * 10 ** e

# list of tuples, the first element is a compiled regular expression the second
# element returns a token and the original string.
actions = [
    (whitespace, lambda s: (WhitespaceToken, s)),
    (openBrace, lambda s: ('{',s)),
    (closeBrace, lambda s: ('}',s)),
    (openSquare, lambda s: ('[',s)),
    (closeSquare, lambda s: (']',s)),
    (string, lambda s: (StringToken(s), s)),
    (colon, lambda s: (':', s)),
    (comma, lambda s: (',', s)),
    (true, lambda s: (True, s)),
    (false, lambda s: (False, s)),
    (null, lambda s: (None, s)),
    (undefined, lambda s: (None, s)),
    (identifier, lambda s: (IdentifierToken(s), s)),
    (floatNumber, lambda s: (float(s), s)),
    (longNumber, lambda s: (jsonlong(s), s)),
]
def tokenise(s):
    tokens = []
    while s:
        for regexp, action in actions:
            m = regexp.match(s)
            if m:
                tok, tokstr = action(m.group(0))
                break
        else:
            raise ValueError, "Invalid Input, %r" % (s[:10],)

        if tok is not WhitespaceToken:
            tokens.append(tok)
        s = s[len(tokstr):]

    return tokens

def accept(want, tokens):
    t = tokens.pop(0)
    if want != t:
        raise ParseError, "Unexpected %r, %s expected" % (t , want)

def parseValue(tokens):
    if tokens[0] == '{':
        return parseObject(tokens)

    if tokens[0] == '[':
        return parseList(tokens)

    if tokens[0] in (True, False, None):
        return tokens.pop(0), tokens

    if type(tokens[0]) == StringToken:
        return parseString(tokens)

    if type(tokens[0]) in (int, float, long):
        return tokens.pop(0), tokens

    raise ParseError, "Unexpected %r" % tokens[0]


_stringExpr = re.compile(
    ur'(?:\\x(?P<unicode>[a-fA-F0-9]{2})) # Match hex-escaped unicode' u'\n'
    ur'|' u'\n'
    ur'(?:\\u(?P<unicode2>[a-fA-F0-9]{4})) # Match hex-escaped high unicode' u'\n'
    ur'|' u'\n'
    ur'(?P<control>\\[fbntr\\"]) # Match escaped control characters' u'\n',
    re.VERBOSE)

_controlMap = {
    u'\\f': u'\f',
    u'\\b': u'\b',
    u'\\n': u'\n',
    u'\\t': u'\t',
    u'\\r': u'\r',
    u'\\"': u'"',
    u'\\\\': u'\\',
    }

def _stringSub(m):
    u = m.group('unicode')
    if u is None:
        u = m.group('unicode2')
    if u is not None:
        return unichr(int(u, 16))
    c = m.group('control')
    return _controlMap[c]


def parseString(tokens):
    if type(tokens[0]) is not StringToken:
        raise ParseError, "Unexpected %r" % tokens[0]
    s = _stringExpr.sub(_stringSub, tokens.pop(0)[1:-1].decode('utf-8'))
    return s, tokens


def parseIdentifier(tokens):
    if type(tokens[0]) is not IdentifierToken:
        raise ParseError("Unexpected %r" % (tokens[0],))
    return tokens.pop(0), tokens


def parseList(tokens):
    l = []
    tokens.pop(0)
    first = True
    while tokens[0] != ']':
        if not first:
            accept(',', tokens)
        first = False

        value, tokens = parseValue(tokens)
        l.append(value)

    accept(']', tokens)
    return l, tokens


def parseObject(tokens):
    o = {}
    tokens.pop(0)
    first = True
    while tokens[0] != '}':
        if not first:
            accept(',', tokens)
        first = False

        name, tokens = parseString(tokens)
        accept(':', tokens)
        value, tokens = parseValue(tokens)
        o[name] = value

    accept('}', tokens)
    return o, tokens


def parse(s):
    """
    Return the object represented by the JSON-encoded string C{s}.
    """
    tokens = tokenise(s)
    value, tokens = parseValue(tokens)
    if tokens:
        raise ParseError, "Unexpected %r" % tokens[0]
    return value

class CycleError(Exception):
    pass

_translation = dict([(o, u'\\x%02x' % (o,)) for o in range(0x20)])

# Characters which cannot appear as literals in the output
_translation.update({
    ord(u'\\'): u'\\\\',
    ord(u'"'): ur'\"',
    ord(u'\f'): ur'\f',
    ord(u'\b'): ur'\b',
    ord(u'\n'): ur'\n',
    ord(u'\t'): ur'\t',
    ord(u'\r'): ur'\r',
    # The next two are sneaky, see
    # http://timelessrepo.com/json-isnt-a-javascript-subset
    ord(u'\u2028'): u'\\u2028',
    ord(u'\u2029'): u'\\u2029',
    })

def stringEncode(s):
    return s.translate(_translation).encode('utf-8')


def _serialize(obj, w, seen):
    from nevow import athena

    if isinstance(obj, types.BooleanType):
        if obj:
            w('true')
        else:
            w('false')
    elif isinstance(obj, (int, long, float)):
        w(str(obj))
    elif isinstance(obj, unicode):
        w('"')
        w(stringEncode(obj))
        w('"')
    elif isinstance(obj, types.NoneType):
        w('null')
    elif id(obj) in seen:
        raise CycleError(type(obj))
    elif isinstance(obj, (tuple, list)):
        w('[')
        for n, e in enumerate(obj):
            _serialize(e, w, seen)
            if n != len(obj) - 1:
                w(',')
        w(']')
    elif isinstance(obj, dict):
        w('{')
        for n, (k, v) in enumerate(obj.iteritems()):
            _serialize(k, w, seen)
            w(':')
            _serialize(v, w, seen)
            if n != len(obj) - 1:
                w(',')
        w('}')
    elif isinstance(obj, (athena.LiveFragment, athena.LiveElement)):
        _serialize(obj._structured(), w, seen)
    elif isinstance(obj, (rend.Fragment, page.Element)):
        def _w(s):
            w(stringEncode(s.decode('utf-8')))
        wrapper = tags.div(xmlns="http://www.w3.org/1999/xhtml")
        w('"')
        for _ in _flat.flatten(None, _w, wrapper[obj], False, False):
            pass
        w('"')
    else:
        transportable = IAthenaTransportable(obj, None)
        if transportable is not None:
            w('(new ' + transportable.jsClass.encode('ascii') + '(')
            arguments = transportable.getInitialArguments()
            for n, e in enumerate(arguments):
                _serialize(e, w, seen)
                if n != len(arguments) - 1:
                    w(',')
            w('))')
        else:
            raise TypeError("Unsupported type %r: %r" % (type(obj), obj))



_undefined = object()
def serialize(obj=_undefined, **kw):
    """
    JSON-encode an object.

    @param obj: None, True, False, an int, long, float, unicode string,
    list, tuple, or dictionary the JSON-encoded form of which will be
    returned.
    """
    if obj is _undefined:
        obj = kw
    L = []
    _serialize(obj, L.append, {})
    return ''.join(L)

__all__ = ['parse', 'serialize']
