/**
 * From https://github.com/Microsoft/node-jsonc-parser/blob/master/src/main.ts
 *
 * Just removed vscode-nls dependency 🌹
 */

/** BAS the function that was provided by vscode-nls */
const localize = (a: string, b: string) => b;

/**
 * Rest is copy paste 🌹
 */

export enum ScanError {
    None,
    UnexpectedEndOfComment,
    UnexpectedEndOfString,
    UnexpectedEndOfNumber,
    InvalidUnicode,
    InvalidEscapeCharacter
}

export enum SyntaxKind {
    Unknown = 0,
    OpenBraceToken,
    CloseBraceToken,
    OpenBracketToken,
    CloseBracketToken,
    CommaToken,
    ColonToken,
    NullKeyword,
    TrueKeyword,
    FalseKeyword,
    StringLiteral,
    NumericLiteral,
    LineCommentTrivia,
    BlockCommentTrivia,
    LineBreakTrivia,
    Trivia,
    EOF
}

/**
 * The scanner object, representing a JSON scanner at a position in the input string.
 */
export interface JSONScanner {
    /**
     * Sets the scan position to a new offset. A call to 'scan' is needed to get the first token.
     */
    setPosition(pos: number);
    /**
     * Read the next token. Returns the tolen code.
     */
    scan(): SyntaxKind;
    /**
     * Returns the current scan position, which is after the last read token.
     */
    getPosition(): number;
    /**
     * Returns the last read token.
     */
    getToken(): SyntaxKind;
    /**
     * Returns the last read token value. The value for strings is the decoded string content. For numbers its of type number, for boolean it's true or false.
     */
    getTokenValue(): string;
    /**
     * The start offset of the last read token.
     */
    getTokenOffset(): number;
    /**
     * The length of the last read token.
     */
    getTokenLength(): number;
    /**
     * An error code of the last scan.
     */
    getTokenError(): ScanError;
}
/**
 * Creates a JSON scanner on the given text.
 * If ignoreTrivia is set, whitespaces or comments are ignored.
 */
export function createScanner(text: string, ignoreTrivia: boolean = false): JSONScanner {

    let pos = 0,
        len = text.length,
        value: string = '',
        tokenOffset = 0,
        token: SyntaxKind = SyntaxKind.Unknown,
        scanError: ScanError = ScanError.None;

    function scanHexDigits(count: number, exact?: boolean): number {
        let digits = 0;
        let value = 0;
        while (digits < count || !exact) {
            let ch = text.charCodeAt(pos);
            if (ch >= CharacterCodes._0 && ch <= CharacterCodes._9) {
                value = value * 16 + ch - CharacterCodes._0;
            }
            else if (ch >= CharacterCodes.A && ch <= CharacterCodes.F) {
                value = value * 16 + ch - CharacterCodes.A + 10;
            }
            else if (ch >= CharacterCodes.a && ch <= CharacterCodes.f) {
                value = value * 16 + ch - CharacterCodes.a + 10;
            }
            else {
                break;
            }
            pos++;
            digits++;
        }
        if (digits < count) {
            value = -1;
        }
        return value;
    }

    function setPosition(newPosition: number) {
        pos = newPosition;
        value = '';
        tokenOffset = 0;
        token = SyntaxKind.Unknown;
        scanError = ScanError.None;
    }

    function scanNumber(): string {
        let start = pos;
        if (text.charCodeAt(pos) === CharacterCodes._0) {
            pos++;
        } else {
            pos++;
            while (pos < text.length && isDigit(text.charCodeAt(pos))) {
                pos++;
            }
        }
        if (pos < text.length && text.charCodeAt(pos) === CharacterCodes.dot) {
            pos++;
            if (pos < text.length && isDigit(text.charCodeAt(pos))) {
                pos++;
                while (pos < text.length && isDigit(text.charCodeAt(pos))) {
                    pos++;
                }
            } else {
                scanError = ScanError.UnexpectedEndOfNumber;
                return text.substring(start, pos);
            }
        }
        let end = pos;
        if (pos < text.length && (text.charCodeAt(pos) === CharacterCodes.E || text.charCodeAt(pos) === CharacterCodes.e)) {
            pos++;
            if (pos < text.length && text.charCodeAt(pos) === CharacterCodes.plus || text.charCodeAt(pos) === CharacterCodes.minus) {
                pos++;
            }
            if (pos < text.length && isDigit(text.charCodeAt(pos))) {
                pos++;
                while (pos < text.length && isDigit(text.charCodeAt(pos))) {
                    pos++;
                }
                end = pos;
            } else {
                scanError = ScanError.UnexpectedEndOfNumber;
            }
        }
        return text.substring(start, end);
    }

    function scanString(): string {

        let result = '',
            start = pos;

        while (true) {
            if (pos >= len) {
                result += text.substring(start, pos);
                scanError = ScanError.UnexpectedEndOfString;
                break;
            }
            let ch = text.charCodeAt(pos);
            if (ch === CharacterCodes.doubleQuote) {
                result += text.substring(start, pos);
                pos++;
                break;
            }
            if (ch === CharacterCodes.backslash) {
                result += text.substring(start, pos);
                pos++;
                if (pos >= len) {
                    scanError = ScanError.UnexpectedEndOfString;
                    break;
                }
                ch = text.charCodeAt(pos++);
                switch (ch) {
                    case CharacterCodes.doubleQuote:
                        result += '\"';
                        break;
                    case CharacterCodes.backslash:
                        result += '\\';
                        break;
                    case CharacterCodes.slash:
                        result += '/';
                        break;
                    case CharacterCodes.b:
                        result += '\b';
                        break;
                    case CharacterCodes.f:
                        result += '\f';
                        break;
                    case CharacterCodes.n:
                        result += '\n';
                        break;
                    case CharacterCodes.r:
                        result += '\r';
                        break;
                    case CharacterCodes.t:
                        result += '\t';
                        break;
                    case CharacterCodes.u:
                        let ch = scanHexDigits(4, true);
                        if (ch >= 0) {
                            result += String.fromCharCode(ch);
                        } else {
                            scanError = ScanError.InvalidUnicode;
                        }
                        break;
                    default:
                        scanError = ScanError.InvalidEscapeCharacter;
                }
                start = pos;
                continue;
            }
            if (isLineBreak(ch)) {
                result += text.substring(start, pos);
                scanError = ScanError.UnexpectedEndOfString;
                break;
            }
            pos++;
        }
        return result;
    }

    function scanNext(): SyntaxKind {

        value = '';
        scanError = ScanError.None;

        tokenOffset = pos;

        if (pos >= len) {
            // at the end
            tokenOffset = len;
            return token = SyntaxKind.EOF;
        }

        let code = text.charCodeAt(pos);
        // trivia: whitespace
        if (isWhiteSpace(code)) {
            do {
                pos++;
                value += String.fromCharCode(code);
                code = text.charCodeAt(pos);
            } while (isWhiteSpace(code));

            return token = SyntaxKind.Trivia;
        }

        // trivia: newlines
        if (isLineBreak(code)) {
            pos++;
            value += String.fromCharCode(code);
            if (code === CharacterCodes.carriageReturn && text.charCodeAt(pos) === CharacterCodes.lineFeed) {
                pos++;
                value += '\n';
            }
            return token = SyntaxKind.LineBreakTrivia;
        }

        switch (code) {
            // tokens: []{}:,
            case CharacterCodes.openBrace:
                pos++;
                return token = SyntaxKind.OpenBraceToken;
            case CharacterCodes.closeBrace:
                pos++;
                return token = SyntaxKind.CloseBraceToken;
            case CharacterCodes.openBracket:
                pos++;
                return token = SyntaxKind.OpenBracketToken;
            case CharacterCodes.closeBracket:
                pos++;
                return token = SyntaxKind.CloseBracketToken;
            case CharacterCodes.colon:
                pos++;
                return token = SyntaxKind.ColonToken;
            case CharacterCodes.comma:
                pos++;
                return token = SyntaxKind.CommaToken;

            // strings
            case CharacterCodes.doubleQuote:
                pos++;
                value = scanString();
                return token = SyntaxKind.StringLiteral;

            // comments
            case CharacterCodes.slash:
                let start = pos - 1;
                // Single-line comment
                if (text.charCodeAt(pos + 1) === CharacterCodes.slash) {
                    pos += 2;

                    while (pos < len) {
                        if (isLineBreak(text.charCodeAt(pos))) {
                            break;
                        }
                        pos++;

                    }
                    value = text.substring(start, pos);
                    return token = SyntaxKind.LineCommentTrivia;
                }

                // Multi-line comment
                if (text.charCodeAt(pos + 1) === CharacterCodes.asterisk) {
                    pos += 2;

                    let safeLength = len - 1; // For lookahead.
                    let commentClosed = false;
                    while (pos < safeLength) {
                        let ch = text.charCodeAt(pos);

                        if (ch === CharacterCodes.asterisk && text.charCodeAt(pos + 1) === CharacterCodes.slash) {
                            pos += 2;
                            commentClosed = true;
                            break;
                        }
                        pos++;
                    }

                    if (!commentClosed) {
                        pos++;
                        scanError = ScanError.UnexpectedEndOfComment;
                    }

                    value = text.substring(start, pos);
                    return token = SyntaxKind.BlockCommentTrivia;
                }
                // just a single slash
                value += String.fromCharCode(code);
                pos++;
                return token = SyntaxKind.Unknown;

            // numbers
            case CharacterCodes.minus:
                value += String.fromCharCode(code);
                pos++;
                if (pos === len || !isDigit(text.charCodeAt(pos))) {
                    return token = SyntaxKind.Unknown;
                }
            // found a minus, followed by a number so
            // we fall through to proceed with scanning
            // numbers
            case CharacterCodes._0:
            case CharacterCodes._1:
            case CharacterCodes._2:
            case CharacterCodes._3:
            case CharacterCodes._4:
            case CharacterCodes._5:
            case CharacterCodes._6:
            case CharacterCodes._7:
            case CharacterCodes._8:
            case CharacterCodes._9:
                value += scanNumber();
                return token = SyntaxKind.NumericLiteral;
            // literals and unknown symbols
            default:
                // is a literal? Read the full word.
                while (pos < len && isUnknownContentCharacter(code)) {
                    pos++;
                    code = text.charCodeAt(pos);
                }
                if (tokenOffset !== pos) {
                    value = text.substring(tokenOffset, pos);
                    // keywords: true, false, null
                    switch (value) {
                        case 'true': return token = SyntaxKind.TrueKeyword;
                        case 'false': return token = SyntaxKind.FalseKeyword;
                        case 'null': return token = SyntaxKind.NullKeyword;
                    }
                    return token = SyntaxKind.Unknown;
                }
                // some
                value += String.fromCharCode(code);
                pos++;
                return token = SyntaxKind.Unknown;
        }
    }

    function isUnknownContentCharacter(code: CharacterCodes) {
        if (isWhiteSpace(code) || isLineBreak(code)) {
            return false;
        }
        switch (code) {
            case CharacterCodes.closeBrace:
            case CharacterCodes.closeBracket:
            case CharacterCodes.openBrace:
            case CharacterCodes.openBracket:
            case CharacterCodes.doubleQuote:
            case CharacterCodes.colon:
            case CharacterCodes.comma:
                return false;
        }
        return true;
    }


    function scanNextNonTrivia(): SyntaxKind {
        let result: SyntaxKind;
        do {
            result = scanNext();
        } while (result >= SyntaxKind.LineCommentTrivia && result <= SyntaxKind.Trivia);
        return result;
    }

    return {
        setPosition: setPosition,
        getPosition: () => pos,
        scan: ignoreTrivia ? scanNextNonTrivia : scanNext,
        getToken: () => token,
        getTokenValue: () => value,
        getTokenOffset: () => tokenOffset,
        getTokenLength: () => pos - tokenOffset,
        getTokenError: () => scanError
    };
}

function isWhiteSpace(ch: number): boolean {
    return ch === CharacterCodes.space || ch === CharacterCodes.tab || ch === CharacterCodes.verticalTab || ch === CharacterCodes.formFeed ||
        ch === CharacterCodes.nonBreakingSpace || ch === CharacterCodes.ogham || ch >= CharacterCodes.enQuad && ch <= CharacterCodes.zeroWidthSpace ||
        ch === CharacterCodes.narrowNoBreakSpace || ch === CharacterCodes.mathematicalSpace || ch === CharacterCodes.ideographicSpace || ch === CharacterCodes.byteOrderMark;
}

function isLineBreak(ch: number): boolean {
    return ch === CharacterCodes.lineFeed || ch === CharacterCodes.carriageReturn || ch === CharacterCodes.lineSeparator || ch === CharacterCodes.paragraphSeparator;
}

function isDigit(ch: number): boolean {
    return ch >= CharacterCodes._0 && ch <= CharacterCodes._9;
}

function isLetter(ch: number): boolean {
    return ch >= CharacterCodes.a && ch <= CharacterCodes.z || ch >= CharacterCodes.A && ch <= CharacterCodes.Z;
}

enum CharacterCodes {
    nullCharacter = 0,
    maxAsciiCharacter = 0x7F,

    lineFeed = 0x0A,              // \n
    carriageReturn = 0x0D,        // \r
    lineSeparator = 0x2028,
    paragraphSeparator = 0x2029,

    // REVIEW: do we need to support this?  The scanner doesn't, but our IText does.  This seems
    // like an odd disparity?  (Or maybe it's completely fine for them to be different).
    nextLine = 0x0085,

    // Unicode 3.0 space characters
    space = 0x0020,   // " "
    nonBreakingSpace = 0x00A0,   //
    enQuad = 0x2000,
    emQuad = 0x2001,
    enSpace = 0x2002,
    emSpace = 0x2003,
    threePerEmSpace = 0x2004,
    fourPerEmSpace = 0x2005,
    sixPerEmSpace = 0x2006,
    figureSpace = 0x2007,
    punctuationSpace = 0x2008,
    thinSpace = 0x2009,
    hairSpace = 0x200A,
    zeroWidthSpace = 0x200B,
    narrowNoBreakSpace = 0x202F,
    ideographicSpace = 0x3000,
    mathematicalSpace = 0x205F,
    ogham = 0x1680,

    _ = 0x5F,
    $ = 0x24,

    _0 = 0x30,
    _1 = 0x31,
    _2 = 0x32,
    _3 = 0x33,
    _4 = 0x34,
    _5 = 0x35,
    _6 = 0x36,
    _7 = 0x37,
    _8 = 0x38,
    _9 = 0x39,

    a = 0x61,
    b = 0x62,
    c = 0x63,
    d = 0x64,
    e = 0x65,
    f = 0x66,
    g = 0x67,
    h = 0x68,
    i = 0x69,
    j = 0x6A,
    k = 0x6B,
    l = 0x6C,
    m = 0x6D,
    n = 0x6E,
    o = 0x6F,
    p = 0x70,
    q = 0x71,
    r = 0x72,
    s = 0x73,
    t = 0x74,
    u = 0x75,
    v = 0x76,
    w = 0x77,
    x = 0x78,
    y = 0x79,
    z = 0x7A,

    A = 0x41,
    B = 0x42,
    C = 0x43,
    D = 0x44,
    E = 0x45,
    F = 0x46,
    G = 0x47,
    H = 0x48,
    I = 0x49,
    J = 0x4A,
    K = 0x4B,
    L = 0x4C,
    M = 0x4D,
    N = 0x4E,
    O = 0x4F,
    P = 0x50,
    Q = 0x51,
    R = 0x52,
    S = 0x53,
    T = 0x54,
    U = 0x55,
    V = 0x56,
    W = 0x57,
    X = 0x58,
    Y = 0x59,
    Z = 0x5a,

    ampersand = 0x26,             // &
    asterisk = 0x2A,              // *
    at = 0x40,                    // @
    backslash = 0x5C,             // \
    bar = 0x7C,                   // |
    caret = 0x5E,                 // ^
    closeBrace = 0x7D,            // }
    closeBracket = 0x5D,          // ]
    closeParen = 0x29,            // )
    colon = 0x3A,                 // :
    comma = 0x2C,                 // ,
    dot = 0x2E,                   // .
    doubleQuote = 0x22,           // "
    equals = 0x3D,                // =
    exclamation = 0x21,           // !
    greaterThan = 0x3E,           // >
    lessThan = 0x3C,              // <
    minus = 0x2D,                 // -
    openBrace = 0x7B,             // {
    openBracket = 0x5B,           // [
    openParen = 0x28,             // (
    percent = 0x25,               // %
    plus = 0x2B,                  // +
    question = 0x3F,              // ?
    semicolon = 0x3B,             // ;
    singleQuote = 0x27,           // '
    slash = 0x2F,                 // /
    tilde = 0x7E,                 // ~

    backspace = 0x08,             // \b
    formFeed = 0x0C,              // \f
    byteOrderMark = 0xFEFF,
    tab = 0x09,                   // \t
    verticalTab = 0x0B,           // \v
}


/**
 * Takes JSON with JavaScript-style comments and remove
 * them. Optionally replaces every none-newline character
 * of comments with a replaceCharacter
 */
export function stripComments(text: string, replaceCh?: string): string {

    let _scanner = createScanner(text),
        parts: string[] = [],
        kind: SyntaxKind,
        offset = 0,
        pos: number;

    do {
        pos = _scanner.getPosition();
        kind = _scanner.scan();
        switch (kind) {
            case SyntaxKind.LineCommentTrivia:
            case SyntaxKind.BlockCommentTrivia:
            case SyntaxKind.EOF:
                if (offset !== pos) {
                    parts.push(text.substring(offset, pos));
                }
                if (replaceCh !== void 0) {
                    parts.push(_scanner.getTokenValue().replace(/[^\r\n]/g, replaceCh));
                }
                offset = _scanner.getPosition();
                break;
        }
    } while (kind !== SyntaxKind.EOF);

    return parts.join('');
}

export enum ParseErrorCode {
    InvalidSymbol,
    InvalidNumberFormat,
    PropertyNameExpected,
    ValueExpected,
    ColonExpected,
    CommaExpected,
    CloseBraceExpected,
    CloseBracketExpected,
    EndOfFileExpected
}

export function getParseErrorMessage(errorCode: ParseErrorCode): string {
    switch (errorCode) {
        case ParseErrorCode.InvalidSymbol: return localize('error.invalidSymbol', 'Invalid symbol');
        case ParseErrorCode.InvalidNumberFormat: return localize('error.invalidNumberFormat', 'Invalid number format');
        case ParseErrorCode.PropertyNameExpected: return localize('error.propertyNameExpected', 'Property name expected');
        case ParseErrorCode.ValueExpected: return localize('error.valueExpected', 'Value expected');
        case ParseErrorCode.ColonExpected: return localize('error.colonExpected', 'Colon expected');
        case ParseErrorCode.CommaExpected: return localize('error.commaExpected', 'Comma expected');
        case ParseErrorCode.CloseBraceExpected: return localize('error.closeBraceExpected', 'Closing brace expected');
        case ParseErrorCode.CloseBracketExpected: return localize('error.closeBracketExpected', 'Closing bracket expected');
        case ParseErrorCode.EndOfFileExpected: return localize('error.endOfFileExpected', 'End of file expected');
        default:
            return '';
    }
}

export type NodeType = "object" | "array" | "property" | "string" | "number" | "null";

export interface Node {
    type: NodeType;
    value: any;
    offset: number;
    length: number;
    columnOffset?: number;
}

export interface Location {
    previousNode?: Node;
    segments: string[];
    matches: (segments: string[]) => boolean;
    completeProperty: boolean;
}


/**
 * For a given offset, evaluate the location in the JSON document. Each segment in a location is either a property names or an array accessors.
 */
export function getLocation(text: string, position: number): Location {
    let segments: string[] = [];
    let earlyReturnException = new Object();
    let previousNode: Node = void 0;
    const previousNodeInst: Node = {
        value: void 0,
        offset: void 0,
        length: void 0,
        type: void 0
    };
    let completeProperty = false;
    let hasComma = false;
    function setPreviousNode(value: string, offset: number, length: number, type: NodeType) {
        previousNodeInst.value = value;
        previousNodeInst.offset = offset;
        previousNodeInst.length = length;
        previousNodeInst.type = type;
        previousNodeInst.columnOffset = void 0;
        previousNode = previousNodeInst;
    }
    try {

        visit(text, {
            onObjectBegin: (offset: number, length: number) => {
                if (position <= offset) {
                    throw earlyReturnException;
                }
                previousNode = void 0;
                completeProperty = position > offset;
                hasComma = false;
            },
            onObjectProperty: (name: string, offset: number, length: number) => {
                if (position < offset) {
                    throw earlyReturnException;
                }
                setPreviousNode(name, offset, length, 'property');
                hasComma = false;
                segments.push(name);
                if (position <= offset + length) {
                    throw earlyReturnException;
                }
            },
            onObjectEnd: (offset: number, length: number) => {
                if (position <= offset) {
                    throw earlyReturnException;
                }
                previousNode = void 0;
                if (!hasComma) {
                    segments.pop();
                }
                hasComma = false;
            },
            onArrayBegin: (offset: number, length: number) => {
                if (position <= offset) {
                    throw earlyReturnException;
                }
                previousNode = void 0;
                segments.push('[0]');
                hasComma = false;
            },
            onArrayEnd: (offset: number, length: number) => {
                if (position <= offset) {
                    throw earlyReturnException;
                }
                previousNode = void 0;
                if (!hasComma) {
                    segments.pop();
                }
                hasComma = false;
            },
            onLiteralValue: (value: any, offset: number, length: number) => {
                if (position < offset) {
                    throw earlyReturnException;
                }
                setPreviousNode(value, offset, length, value === null ? 'null' : (typeof value === 'string' ? 'string' : 'number'));
                if (position <= offset + length) {
                    throw earlyReturnException;
                }
            },
            onSeparator: (sep: string, offset: number, length: number) => {
                if (position <= offset) {
                    throw earlyReturnException;
                }
                if (sep === ':' && previousNode.type === 'property') {
                    previousNode.columnOffset = offset;
                    completeProperty = false;
                    previousNode = void 0;
                } else if (sep === ',') {
                    let last = segments.pop();
                    if (last[0] === '[' && last[last.length - 1] === ']') {
                        segments.push('[' + (parseInt(last.substr(1, last.length - 2)) + 1) + ']');
                    } else {
                        completeProperty = true;
                    }
                    previousNode = void 0;
                    hasComma = true;
                }
            }
        });
    } catch (e) {
        if (e !== earlyReturnException) {
            throw e;
        }
    }
    return {
        segments,
        previousNode,
        completeProperty,
        matches: (pattern: string[]) => {
            let k = 0;
            for (let i = 0; k < pattern.length && i < segments.length; i++) {
                if (pattern[k] === segments[i] || pattern[k] === '*') {
                    k++;
                } else if (pattern[k] !== '**') {
                    return false;
                }
            }
            return k === pattern.length;
        }
    };
}

export interface ParseOptions {
    disallowComments?: boolean;
}

/**
 * Parses the given text and returns the object the JSON content represents. On invalid input, the parser tries to be as fault lolerant as possible, but still return a result.
 * Therefore always check the errors list to find out if the input was valid.
 */
export function parse(text: string, errors: { error: ParseErrorCode; }[] = [], options?: ParseOptions): any {
    let currentProperty: string = null;
    let currentParent: any = [];
    let previousParents: any[] = [];

    function onValue(value: any) {
        if (Array.isArray(currentParent)) {
            (<any[]>currentParent).push(value);
        } else if (currentProperty) {
            currentParent[currentProperty] = value;
        }
    }

    let visitor = {
        onObjectBegin: () => {
            let object = {};
            onValue(object);
            previousParents.push(currentParent);
            currentParent = object;
            currentProperty = null;
        },
        onObjectProperty: (name: string) => {
            currentProperty = name;
        },
        onObjectEnd: () => {
            currentParent = previousParents.pop();
        },
        onArrayBegin: () => {
            let array = [];
            onValue(array);
            previousParents.push(currentParent);
            currentParent = array;
            currentProperty = null;
        },
        onArrayEnd: () => {
            currentParent = previousParents.pop();
        },
        onLiteralValue: onValue,
        onError: (error: ParseErrorCode) => {
            errors.push({ error: error });
        }
    };
    visit(text, visitor, options);
    return currentParent[0];
}

/**
 * Parses the given text and invokes the visitor functions for each object, array and literal reached.
 */
export function visit(text: string, visitor: JSONVisitor, options?: ParseOptions): any {

    let _scanner = createScanner(text, false);

    function toNoArgVisit(visitFunction: (offset: number, length: number) => void): () => void {
        return visitFunction ? () => visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength()) : () => true;
    }
    function toOneArgVisit<T>(visitFunction: (arg: T, offset: number, length: number) => void): (arg: T) => void {
        return visitFunction ? (arg: T) => visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength()) : () => true;
    }

    let onObjectBegin = toNoArgVisit(visitor.onObjectBegin),
        onObjectProperty = toOneArgVisit(visitor.onObjectProperty),
        onObjectEnd = toNoArgVisit(visitor.onObjectEnd),
        onArrayBegin = toNoArgVisit(visitor.onArrayBegin),
        onArrayEnd = toNoArgVisit(visitor.onArrayEnd),
        onLiteralValue = toOneArgVisit(visitor.onLiteralValue),
        onSeparator = toOneArgVisit(visitor.onSeparator),
        onError = toOneArgVisit(visitor.onError);

    let disallowComments = options && options.disallowComments;
    function scanNext(): SyntaxKind {
        while (true) {
            let token = _scanner.scan();
            switch (token) {
                case SyntaxKind.LineCommentTrivia:
                case SyntaxKind.BlockCommentTrivia:
                    if (disallowComments) {
                        handleError(ParseErrorCode.InvalidSymbol);
                    }
                    break;
                case SyntaxKind.Unknown:
                    handleError(ParseErrorCode.InvalidSymbol);
                    break;
                case SyntaxKind.Trivia:
                case SyntaxKind.LineBreakTrivia:
                    break;
                default:
                    return token;
            }
        }
    }

    function handleError(error: ParseErrorCode, skipUntilAfter: SyntaxKind[] = [], skipUntil: SyntaxKind[] = []): void {
        onError(error);
        if (skipUntilAfter.length + skipUntil.length > 0) {
            let token = _scanner.getToken();
            while (token !== SyntaxKind.EOF) {
                if (skipUntilAfter.indexOf(token) !== -1) {
                    scanNext();
                    break;
                } else if (skipUntil.indexOf(token) !== -1) {
                    break;
                }
                token = scanNext();
            }
        }
    }

    function parseString(isValue: boolean): boolean {
        if (_scanner.getToken() !== SyntaxKind.StringLiteral) {
            return false;
        }
        let value = _scanner.getTokenValue();
        if (isValue) {
            onLiteralValue(value);
        } else {
            onObjectProperty(value);
        }
        scanNext();
        return true;
    }

    function parseLiteral(): boolean {
        switch (_scanner.getToken()) {
            case SyntaxKind.NumericLiteral:
                let value = 0;
                try {
                    value = JSON.parse(_scanner.getTokenValue());
                    if (typeof value !== 'number') {
                        handleError(ParseErrorCode.InvalidNumberFormat);
                        value = 0;
                    }
                } catch (e) {
                    handleError(ParseErrorCode.InvalidNumberFormat);
                }
                onLiteralValue(value);
                break;
            case SyntaxKind.NullKeyword:
                onLiteralValue(null);
                break;
            case SyntaxKind.TrueKeyword:
                onLiteralValue(true);
                break;
            case SyntaxKind.FalseKeyword:
                onLiteralValue(false);
                break;
            default:
                return false;
        }
        scanNext();
        return true;
    }

    function parseProperty(): boolean {
        if (!parseString(false)) {
            handleError(ParseErrorCode.PropertyNameExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]);
            return false;
        }
        if (_scanner.getToken() === SyntaxKind.ColonToken) {
            onSeparator(':');
            scanNext(); // consume colon

            if (!parseValue()) {
                handleError(ParseErrorCode.ValueExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]);
            }
        } else {
            handleError(ParseErrorCode.ColonExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]);
        }
        return true;
    }

    function parseObject(): boolean {
        if (_scanner.getToken() !== SyntaxKind.OpenBraceToken) {
            return false;
        }
        onObjectBegin();
        scanNext(); // consume open brace

        let needsComma = false;
        while (_scanner.getToken() !== SyntaxKind.CloseBraceToken && _scanner.getToken() !== SyntaxKind.EOF) {
            if (_scanner.getToken() === SyntaxKind.CommaToken) {
                if (!needsComma) {
                    handleError(ParseErrorCode.ValueExpected, [], []);
                }
                onSeparator(',');
                scanNext(); // consume comma
            } else if (needsComma) {
                handleError(ParseErrorCode.CommaExpected, [], []);
            }
            if (!parseProperty()) {
                handleError(ParseErrorCode.ValueExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]);
            }
            needsComma = true;
        }
        onObjectEnd();
        if (_scanner.getToken() !== SyntaxKind.CloseBraceToken) {
            handleError(ParseErrorCode.CloseBraceExpected, [SyntaxKind.CloseBraceToken], []);
        } else {
            scanNext(); // consume close brace
        }
        return true;
    }

    function parseArray(): boolean {
        if (_scanner.getToken() !== SyntaxKind.OpenBracketToken) {
            return false;
        }
        onArrayBegin();
        scanNext(); // consume open bracket

        let needsComma = false;
        while (_scanner.getToken() !== SyntaxKind.CloseBracketToken && _scanner.getToken() !== SyntaxKind.EOF) {
            if (_scanner.getToken() === SyntaxKind.CommaToken) {
                if (!needsComma) {
                    handleError(ParseErrorCode.ValueExpected, [], []);
                }
                onSeparator(',');
                scanNext(); // consume comma
            } else if (needsComma) {
                handleError(ParseErrorCode.CommaExpected, [], []);
            }
            if (!parseValue()) {
                handleError(ParseErrorCode.ValueExpected, [], [SyntaxKind.CloseBracketToken, SyntaxKind.CommaToken]);
            }
            needsComma = true;
        }
        onArrayEnd();
        if (_scanner.getToken() !== SyntaxKind.CloseBracketToken) {
            handleError(ParseErrorCode.CloseBracketExpected, [SyntaxKind.CloseBracketToken], []);
        } else {
            scanNext(); // consume close bracket
        }
        return true;
    }

    function parseValue(): boolean {
        return parseArray() || parseObject() || parseString(true) || parseLiteral();
    }

    scanNext();
    if (!parseValue()) {
        handleError(ParseErrorCode.ValueExpected, [], []);
        return false;
    }
    if (_scanner.getToken() !== SyntaxKind.EOF) {
        handleError(ParseErrorCode.EndOfFileExpected, [], []);
    }
    return true;
}

export interface JSONVisitor {
    /**
     * Invoked when an open brace is encountered and an object is started. The offset and length represent the location of the open brace.
     */
    onObjectBegin?: (offset: number, length: number) => void;

    /**
     * Invoked when a property is encountered. The offset and length represent the location of the property name.
     */
    onObjectProperty?: (property: string, offset: number, length: number) => void;

    /**
     * Invoked when a closing brace is encountered and an object is completed. The offset and length represent the location of the closing brace.
     */
    onObjectEnd?: (offset: number, length: number) => void;

    /**
     * Invoked when an open bracket is encountered. The offset and length represent the location of the open bracket.
     */
    onArrayBegin?: (offset: number, length: number) => void;

    /**
     * Invoked when a closing bracket is encountered. The offset and length represent the location of the closing bracket.
     */
    onArrayEnd?: (offset: number, length: number) => void;

    /**
     * Invoked when a literal value is encountered. The offset and length represent the location of the literal value.
     */
    onLiteralValue?: (value: any, offset: number, length: number) => void;

    /**
     * Invoked when a comma or colon separator is encountered. The offset and length represent the location of the separator.
     */
    onSeparator?: (charcter: string, offset: number, length: number) => void;

    /**
     * Invoked on an error.
     */
    onError?: (error: ParseErrorCode, offset: number, length: number) => void;
}
