
Scanner = {src: String, i: int};

fun newScanner(src: String) {
    scan = {src: src, i: 0};
    assert(istypeof(Scanner, scan));
    return scan;
}

fun isDigit(char: ?String) {
    if char == none:
        return false;
    return string.ord(char) >= string.ord('0')
       and string.ord(char) <= string.ord('9');
}

fun isSpace(c: String)
    return c == ' '
        or c == '\t'
        or c == '\n';

fun min(x, y) {
    if x < y:
        return x;
    return y;
}

fun hint(scan: Scanner, n: int = 1) {
    k = scan.i + n;
    if k < count(scan.src):
        return scan.src[k];
    return none;
}

fun current(scan: Scanner)
    return hint(scan, 0);

fun consume(scan: Scanner, n: int = 1) {
    assert(n > 0);
    limit = count(scan.src);
    scan.i = min(scan.i + n, limit);
}

fun consumeSpaces(scan: Scanner)
    while isSpace(current(scan)):
        consume(scan);

fun parseString(scan: Scanner) {

    assert(current(scan) == '"');
    consume(scan); # Consume the opening "

    temp = "";
    char = current(scan);
    while char != '"' and char != none: {
        temp = string.cat(temp, char);
        consume(scan);
        char = current(scan);
    }

    if char == none: {
        val = none;
        err = "Source ended inside a string literal";
    } else {
        assert(char == '"');
        consume(scan);
        val = temp;
        err = none;
    }

    return val, err;
}

fun parseObject(scan: Scanner) {
    
    assert(current(scan) == '{');
    consume(scan); 

    obj = {};
    consumeSpaces(scan);
    if current(scan) != '}': do {
        key, err = parseValue(scan);
        if err != none: 
            return none, err;
        if type(key) != String:
            return none, "Object key isn't a string";

        consumeSpaces(scan);
        if current(scan) != ':':
            return none, "Missing ':' separator after key";
        consume(scan);

        val, err = parseValue(scan);
        if err != none:
            return none, err;

        if obj[key] != none:
            return none, "Duplicate key in object";
        obj[key] = val;

        consumeSpaces(scan);
        if current(scan) == '}':
            done = true;
        else {
            if current(scan) != ',':
                return none, "Invalid character where either ',' or '}' were expected";
            consume(scan); # Consume the ","
            done = false;
        }
    } while not done;
    assert(current(scan) == '}');
    consume(scan);
    return obj;
}

fun parseArray(scan: Scanner) {
    
    assert(current(scan) == '[');
    consume(scan); # Consume the [
    
    array = [];
    
    consumeSpaces(scan);
    if current(scan) != ']': do {

        val, err = parseValue(scan);
        if err != none:
            return none, err;
        array[count(array)] = val;

        consumeSpaces(scan);
        if current(scan) == ']':
            done = true;
        else {
            if current(scan) != ',':
                return none, "Invalid character where either ',' or ']' were expected";
            consume(scan); # Consume the ","
            done = false;
        }
    } while not done;
    assert(current(scan) == ']');
    consume(scan);
    return array;
}

fun parseFalse(scan: Scanner) {
    
    assert(current(scan) == 'f');
    consume(scan);

    if current(scan) != 'a':
        return none, "Invalid character after 'f'";
    consume(scan);

    if current(scan) != 'l':
        return none, "Invalid character after 'fa'";
    consume(scan);

    if current(scan) != 's':
        return none, "Invalid character after 'fal'";
    consume(scan);

    if current(scan) != 'e':
        return none, "Invalid character after 'fals'";
    consume(scan);

    return false;
}

fun parseTrue(scan: Scanner) {
    
    assert(current(scan) == 't');
    consume(scan);

    if current(scan) != 'r':
        return none, "Invalid character after 't'";
    consume(scan);

    if current(scan) != 'u':
        return none, "Invalid character after 'tr'";
    consume(scan);

    if current(scan) != 'e':
        return none, "Invalid character after 'tru'";
    consume(scan);

    return true;
}

fun parseNull(scan: Scanner) {
    
    assert(current(scan) == 'n');
    consume(scan);

    if current(scan) != 'u':
        return none, "Invalid character after 'n'";
    consume(scan);

    if current(scan) != 'l':
        return none, "Invalid character after 'nu'";
    consume(scan);

    if current(scan) != 'l':
        return none, "Invalid character after 'nul'";
    consume(scan);

    return none;
}

fun integerFromDigit(char: String) {
    assert(count(char) == 1);
    return string.ord(char) 
         - string.ord('0');
}

fun parseNumber(scan: Scanner) {

    char = current(scan);
    
    sign = 1;
    if char == '+' or char == '-': {
        
        sign = {'+': 1, '-': -1}[char];
        consume(scan);

        char = current(scan);
        if not isDigit(char):
            return none, "Invalid character after +/-";
    }

    temp = 0.0;
    do {
        assert(isDigit(char));

        n = integerFromDigit(char);
        temp = temp * 10 + n;

        consume(scan);
        char = current(scan);

    } while isDigit(char);

    if char == '.' and isDigit(hint(scan, 1)): {
        
        consume(scan); # Consume the dot
        char = current(scan);
        
        q = 1.0;
        do {
            q = q / 10;
            n = integerFromDigit(char);
            temp = temp + q * n;
            consume(scan);
            char = current(scan);
        } while isDigit(char);
    }

    return sign * temp;
}

fun parseValue(scan: Scanner) {

    table = {
        '"': parseString,
        '{': parseObject,
        '[': parseArray,
        'f': parseFalse,
        't': parseTrue,
        'n': parseNull,
        '+': parseNumber,
        '-': parseNumber,
        '0': parseNumber,
        '1': parseNumber,
        '2': parseNumber,
        '3': parseNumber,
        '4': parseNumber,
        '5': parseNumber,
        '6': parseNumber,
        '7': parseNumber,
        '8': parseNumber,
        '9': parseNumber
    };

    consumeSpaces(scan);
    char = current(scan);
    if char == none: {
        val = none;
        err = "Source ended where a value was expected";
    } else {
        callback = table[char];
        if callback == none: {
            val = none;
            err = "Invalid character where a value was expected";
        } else
            (val, err) = callback(scan);
    }
    return (val, err);
}

fun parse(src: String) {
    scan = newScanner(src);
    val, err = parseValue(scan);
    return val, err;
}