// Original work by Henrik Muehe (c) 2010
//
// CommonJS port by Mikola Lysenko 2013
//
// Modified by Wasif Hasan Baig <pr.wasif@gmail.com> 2015
//
//

// Issues:
//  no comment handling within strings
//  no string concatenation
//  no variable values yet

// Grammar implemented here:
//  bibtex -> (string | preamble | comment | entry)*;
//  string -> '@STRING' '{' key_equals_value '}';
//  preamble -> '@PREAMBLE' '{' value '}';
//  comment -> '@COMMENT' '{' value '}';
//  entry -> '@' key '{' key ',' key_value_list '}';
//  key_value_list -> key_equals_value (',' key_equals_value)*;
//  key_equals_value -> key '=' value;
//  value -> value_quotes | value_braces | key;
//  value_quotes -> '"' .*? '"'; // not quite
//  value_braces -> '{' .*? '"'; // not quite

define(function (require, exports, module) {
    "use strict";

    function BibtexParser() {
        this.pos = 0;
        this.input = "";

        this.entries = {};
        this.comments = [];
        this.strings = {
            JAN: "January",
            FEB: "February",
            MAR: "March",
            APR: "April",
            MAY: "May",
            JUN: "June",
            JUL: "July",
            AUG: "August",
            SEP: "September",
            OCT: "October",
            NOV: "November",
            DEC: "December"
        };
        this.currentKey = "";
        this.currentEntry = "";


        this.setInput = function (t) {
            this.input = t;
        }

        this.getEntries = function () {
            return this.entries;
        }

        this.isWhitespace = function (s) {
            return (s == ' ' || s == '\r' || s == '\t' || s == '\n');
        }

        this.match = function (s) {
            this.skipWhitespace();
            if (this.input.substring(this.pos, this.pos + s.length) == s) {
                this.pos += s.length;
            } else {
                throw "Token mismatch, expected " + s + ", found " + this.input.substring(this.pos);
            }
            this.skipWhitespace();
        }

        this.tryMatch = function (s) {
            this.skipWhitespace();
            if (this.input.substring(this.pos, this.pos + s.length) == s) {
                return true;
            } else {
                return false;
            }
            this.skipWhitespace();
        }

        this.skipWhitespace = function () {
            while (this.isWhitespace(this.input[this.pos])) {
                this.pos++;
            }
            if (this.input[this.pos] == "%") {
                while (this.input[this.pos] != "\n") {
                    this.pos++;
                }
                this.skipWhitespace();
            }
        }

        this.value_braces = function () {
            var bracecount = 0;
            this.match("{");
            var start = this.pos;
            while (true) {
                if (this.input[this.pos] == '}' && this.input[this.pos - 1] != '\\') {
                    if (bracecount > 0) {
                        bracecount--;
                    } else {
                        var end = this.pos;
                        this.match("}");
                        return this.input.substring(start, end);
                    }
                } else if (this.input[this.pos] == '{') {
                    bracecount++;
                } else if (this.pos == this.input.length - 1) {
                    throw "Unterminated value";
                }
                this.pos++;
            }
        }

        this.value_quotes = function () {
            this.match('"');
            var start = this.pos;
            while (true) {
                if (this.input[this.pos] == '"' && this.input[this.pos - 1] != '\\') {
                    var end = this.pos;
                    this.match('"');
                    return this.input.substring(start, end);
                } else if (this.pos == this.input.length - 1) {
                    throw "Unterminated value:" + this.input.substring(start);
                }
                this.pos++;
            }
        }

        this.single_value = function () {
            var start = this.pos;
            if (this.tryMatch("{")) {
                return this.value_braces();
            } else if (this.tryMatch('"')) {
                return this.value_quotes();
            } else {
                var k = this.key();
                if (this.strings[k.toUpperCase()]) {
                    return this.strings[k];
                } else if (k.match("^[0-9]+$")) {
                    return k;
                } else {
                    throw "Value expected:" + this.input.substring(start);
                }
            }
        }

        this.value = function () {
            var values = [];
            values.push(this.single_value());
            while (this.tryMatch("#")) {
                this.match("#");
                values.push(this.single_value());
            }
            return values.join("");
        }

        this.key = function () {
            var start = this.pos;
            while (true) {
                if (this.pos == this.input.length) {
                    throw "Runaway key";
                }

                if (this.input[this.pos].match("[a-zA-Z0-9_:\\./-]")) {
                    this.pos++
                } else {
                    return this.input.substring(start, this.pos).toUpperCase();
                }
            }
        }

        this.key_equals_value = function () {
            var key = this.key();
            if (this.tryMatch("=")) {
                this.match("=");
                var val = this.value();
                return [key, val];
            } else {
                throw "... = value expected, equals sign missing:" + this.input.substring(this.pos);
            }
        }

        this.key_value_list = function () {
            var kv = this.key_equals_value();
            this.entries[this.currentEntry][kv[0]] = kv[1];
            while (this.tryMatch(",")) {
                this.match(",");
                // fixes problems with commas at the end of a list
                if (this.tryMatch("}")) {
                    break;
                }
                kv = this.key_equals_value();
                this.entries[this.currentEntry][kv[0]] = kv[1];
            }
        }

        this.entry_body = function (d) {
            this.currentEntry = this.key();
            this.entries[this.currentEntry] = {
                entryType: d.substring(1)
            };
            this.match(",");
            this.key_value_list();
        }

        this.directive = function () {
            this.match("@");
            return "@" + this.key();
        }

        this.string = function () {
            var kv = this.key_equals_value();
            this.strings[kv[0].toUpperCase()] = kv[1];
        }

        this.preamble = function () {
            this.value();
        }

        this.comment = function () {
            var start = this.pos;
            while (true) {
                if (this.pos == this.input.length) {
                    throw "Runaway comment";
                }

                if (this.input[this.pos] != '}') {
                    this.pos++
                } else {
                    this.comments.push(this.input.substring(start, this.pos));
                    return;
                }
            }
        }

        this.entry = function (d) {
            this.entry_body(d);
        }

        this.bibtex = function () {
            while (this.tryMatch("@")) {
                var d = this.directive().toUpperCase();
                this.match("{");
                if (d == "@STRING") {
                    this.string();
                } else if (d == "@PREAMBLE") {
                    this.preamble();
                } else if (d == "@COMMENT") {
                    this.comment();
                } else {
                    this.entry(d);
                }
                this.match("}");
            }

            this.entries['@comments'] = this.comments;
        }
    }
    
    //Runs the parser
    /**
     * PArses BibTeX string and returns JSON representation
     * @param   {String}  input     BibTeX string
     * @param   {Boolean} stringify A flag indicating whether to stringify values of properties or not
     * @returns {Object}  JSON representation of BibTeX string
     */
    function doParse(input) {
        var b = new BibtexParser()
        b.setInput(input)
        b.bibtex()
        return b.entries
    }

    module.exports = {
        parse: doParse
    }
});