var carto = exports,
    tree = require('./tree'),
    chroma = require('chroma-js'),
    util = require('./util');

//    Token matching is done with the `$` function, which either takes
//    a terminal string or regexp, or a non-terminal function to call.
//    It also takes care of moving all the indices forwards.
carto.Parser = function Parser(env) {
    var input,       // LeSS input string
        i,           // current index in `input`
        j,           // current chunk
        temp,        // temporarily holds a chunk's state, for backtracking
        memo,        // temporarily holds `i`, when backtracking
        chunks,      // chunkified input
        current,     // index of current chunk, in `input`
        parser;

    var that = this;

    function save()    {
        temp = chunks[j];
        memo = i;
        current = i;
    }
    function restore() {
        chunks[j] = temp;
        i = memo;
        current = i;
    }

    function sync() {
        if (i > current) {
            chunks[j] = chunks[j].slice(i - current);
            current = i;
        }
    }
    //
    // Parse from a token, regexp or string, and move forward if match
    //
    function $(tok) {
        var match, length, c, endIndex;

        // Non-terminal
        if (tok instanceof Function) {
            return tok.call(parser.parsers);
        // Terminal
        // Either match a single character in the input,
        // or match a regexp in the current chunk (chunk[j]).
        } else if (typeof(tok) === 'string') {
            match = input.charAt(i) === tok ? tok : null;
            length = 1;
            sync();
        } else {
            sync();

            match = tok.exec(chunks[j]);
            if (match) {
                length = match[0].length;
            } else {
                return null;
            }
        }

        // The match is confirmed, add the match length to `i`,
        // and consume any extra white-space characters (' ' || '\n')
        // which come after that. The reason for this is that LeSS's
        // grammar is mostly white-space insensitive.
        if (match) {
            var mem = i += length;
            endIndex = i + chunks[j].length - length;

            while (i < endIndex) {
                c = input.charCodeAt(i);
                if (! (c === 32 || c === 10 || c === 9)) { break; }
                i++;
            }
            chunks[j] = chunks[j].slice(length + (i - mem));
            current = i;

            if (chunks[j].length === 0 && j < chunks.length - 1) { j++; }

            if (typeof(match) === 'string') {
                return match;
            } else {
                return match.length === 1 ? match[0] : match;
            }
        }
    }

    // Same as $(), but don't change the state of the parser,
    // just return the match.
    function peek(tok) {
        if (typeof(tok) === 'string') {
            return input.charAt(i) === tok;
        } else {
            return !!tok.test(chunks[j]);
        }
    }

    this.env = env = env || {};
    this.env.filename = this.env.filename || null;
    this.env.inputs = this.env.inputs || {};

    // The Parser
    parser = {

        // Parse an input string into an abstract syntax tree.
        // Throws an error on parse errors.
        parse: function(str) {
            var root, error = null;

            i = j = current = 0;
            chunks = [];
            input = str.replace(/\r\n/g, '\n');
            if (env.filename) {
                that.env.inputs[env.filename] = input;
            }

            // Split the input into chunks.
            chunks = (function (chunks) {
                var j = 0,
                    skip = /(?:@\{[\w-]+\}|[^"'`\{\}\/\(\)\\])+/g,
                    comment = /\/\*(?:[^*]|\*+[^\/*])*\*+\/|\/\/.*/g,
                    string = /"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'|`((?:[^`]|\\.)*)`/g,
                    level = 0,
                    match,
                    chunk = chunks[0],
                    inParam;

                for (var i = 0, c, cc; i < input.length;) {
                    skip.lastIndex = i;
                    match = skip.exec(input);
                    if (match) {
                        if (match.index === i) {
                            i += match[0].length;
                            chunk.push(match[0]);
                        }
                    }
                    c = input.charAt(i);
                    comment.lastIndex = string.lastIndex = i;

                    match = string.exec(input)
                    if (match) {
                        if (match.index === i) {
                            i += match[0].length;
                            chunk.push(match[0]);
                            continue;
                        }
                    }

                    if (!inParam && c === '/') {
                        cc = input.charAt(i + 1);
                        if (cc === '/' || cc === '*') {
                            match = comment.exec(input);
                            if (match) {
                                if (match.index === i) {
                                    i += match[0].length;
                                    chunk.push(match[0]);
                                    continue;
                                }
                            }
                        }
                    }

                    switch (c) {
                        case '{':
                            if (!inParam) {
                                level ++;
                            }
                            else {
                                inParam = false;
                            }
                            chunk.push(c);
                            break;
                        case '}':
                            if (!inParam) {
                                level --;
                                chunk.push(c);
                                chunks[++j] = chunk = [];
                            }
                            else {
                                inParam = false;
                                chunk.push(c);
                            }
                            break;
                        case '(':
                            if (!inParam) {
                                inParam = true;
                            }
                            else {
                                inParam = false;
                            }
                            chunk.push(c);
                            break;
                        case ')':
                            if (inParam) {
                                inParam = false;
                            }
                            chunk.push(c);
                            break;
                        default:
                            chunk.push(c);
                    }

                    i++;
                }
                if (level !== 0) {
                    error = {
                        index: i - 1,
                        message: (level > 0) ? "missing closing `}`" : "missing opening `{`",
                        filename: env.filename
                    };
                }

                return chunks.map(function (c) { return c.join(''); });
            })([[]]);

            if (error) {
                util.error(env, error);
                throw new Error('N/A');
            }

            // Start with the primary rule.
            // The whole syntax tree is held under a Ruleset node,
            // with the `root` property set to true, so no `{}` are
            // output.
            root = new tree.Ruleset([], $(this.parsers.primary));
            root.root = true;

            // Get an array of Ruleset objects, flattened
            // and sorted according to specificitySort
            root.toList = (function() {
                return function(env) {
                    env.frames = env.frames || [];


                    // call populates Invalid-caused errors
                    var definitions = this.flatten([], [], env);
                    definitions.sort(specificitySort);
                    return definitions;
                };
            })();

            // Sort rules by specificity: this function expects selectors to be
            // split already.
            //
            // Written to be used as a .sort(Function);
            // argument.
            //
            // [1, 0, 0, 467] > [0, 0, 1, 520]
            var specificitySort = function(a, b) {
                var as = a.specificity;
                var bs = b.specificity;

                if (as[0] != bs[0]) return bs[0] - as[0];
                if (as[1] != bs[1]) return bs[1] - as[1];
                if (as[2] != bs[2]) return bs[2] - as[2];
                return bs[3] - as[3];
            };

            return root;
        },

        // Here in, the parsing rules/functions
        //
        // The basic structure of the syntax tree generated is as follows:
        //
        //   Ruleset ->  Rule -> Value -> Expression -> Entity
        //
        //  In general, most rules will try to parse a token with the `$()` function, and if the return
        //  value is truly, will return a new node, of the relevant type. Sometimes, we need to check
        //  first, before parsing, that's when we use `peek()`.
        parsers: {
            // The `primary` rule is the *entry* and *exit* point of the parser.
            // The rules here can appear at any level of the parse tree.
            //
            // The recursive nature of the grammar is an interplay between the `block`
            // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
            // as represented by this simplified grammar:
            //
            //     primary  →  (ruleset | rule)+
            //     ruleset  →  selector+ block
            //     block    →  '{' primary '}'
            //
            // Only at one point is the primary rule not called from the
            // block rule: at the root level.
            primary: function() {
                var node, root = [];

                while ((node = $(this.rule) || $(this.ruleset) ||
                               $(this.comment)) ||
                               $(/^[\s\n]+/) || (node = $(this.invalid))) {
                    if (node) root.push(node);
                }
                return root;
            },

            invalid: function () {
                var chunk = $(/^[^;\n]*[;\n]/);

                // To fail gracefully, match everything until a semicolon or linebreak.
                if (chunk) {
                    return new tree.Invalid(chunk, memo, null, env.filename);
                }
            },

            // We create a Comment node for CSS comments `/* */`,
            // but keep the LeSS comments `//` silent, by just skipping
            // over them.
            comment: function() {
                if (input.charAt(i) !== '/') return;

                var comment = $(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/);
                if (input.charAt(i + 1) === '/') {
                    return new tree.Comment($(/^\/\/.*/), true);
                } else if (comment) {
                    return new tree.Comment(comment);
                }
            },

            // Entities are tokens which can be found inside an Expression
            entities: {

                // A string, which supports escaping " and ' "milky way" 'he\'s the one!'
                quoted: function() {
                    if (input.charAt(i) !== '"' && input.charAt(i) !== "'") return;
                    var str = $(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/);
                    if (str) {
                        return new tree.Quoted(str[1] || str[2]);
                    }
                },

                // A reference to a Mapnik field, like [NAME]
                // Behind the scenes, this has the same representation, but Carto
                // needs to be careful to warn when unsupported operations are used.
                field: function() {
                    if (! $('[')) return;
                    var field_name = $(/(^[^\]]+)/);
                    if (! $(']')) return;
                    if (field_name) return new tree.Field(field_name[1]);
                },

                // This is a comparison operator
                comparison: function() {
                    var str = $(/^=~|=|!=|<=|>=|<|>/);
                    if (str) {
                        return str;
                    }
                },

                // A catch-all word, such as: hard-light
                // These can start with either a letter or a dash (-),
                // and then contain numbers, underscores, and letters.
                keyword: function() {
                    var k = $(/^[A-Za-z-]+[A-Za-z-0-9_]*/);
                    if (k) { return new tree.Keyword(k); }
                },

                // A function call like rgb(255, 0, 255)
                // The arguments are parsed with the `entities.arguments` parser.
                call: function() {
                    var name, args;

                    if (!(name = /^([\w\-]+|%)\(/.exec(chunks[j]))) return;

                    name = name[1];

                    if (name === 'url') {
                        // url() is handled by the url parser instead
                        return null;
                    } else {
                        i += name.length;
                    }

                    $('('); // Parse the '(' and consume whitespace.

                    args = $(this.entities['arguments']);

                    if (!$(')')) return;

                    if (name) {
                        return new tree.Call(name, args, env.filename, i);
                    }
                },
                // Arguments are comma-separated expressions
                'arguments': function() {
                    var args = [], arg;

                    arg = $(this.expression);
                    while (arg) {
                        args.push(arg);
                        if (! $(',')) { break; }
                        arg = $(this.expression);
                    }

                    return args;
                },
                literal: function() {
                    return $(this.entities.dimension) ||
                        $(this.entities.keywordcolor) ||
                        $(this.entities.hexcolor) ||
                        $(this.entities.quoted);
                },

                // Parse url() tokens
                //
                // We use a specific rule for urls, because they don't really behave like
                // standard function calls. The difference is that the argument doesn't have
                // to be enclosed within a string, so it can't be parsed as an Expression.
                url: function() {
                    var value;

                    if (input.charAt(i) !== 'u' || !$(/^url\(/)) return;
                    value = $(this.entities.quoted) || $(this.entities.variable) ||
                            $(/^[\-\w%@$\/.&=:;#+?~]+/) || '';
                    if (! $(')')) {
                        return new tree.Invalid(value, memo, 'Missing closing ) in URL.', env.filename);
                    } else {
                        return new tree.URL((typeof value.value !== 'undefined' ||
                            value instanceof tree.Variable) ?
                            value : new tree.Quoted(value));
                    }
                },

                // A Variable entity, such as `@fink`, in
                //
                //     width: @fink + 2px
                //
                // We use a different parser for variable definitions,
                // see `parsers.variable`.
                variable: function() {
                    var name, index = i;

                    if (input.charAt(i) === '@' && (name = $(/^@[\w-]+/))) {
                        return new tree.Variable(name, index, env.filename);
                    }
                },

                hexcolor: function() {
                    var rgb;
                    if (input.charAt(i) === '#' && (rgb = $(/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/))) {
                        var hsl = chroma(rgb[0]).hsl();
                        return new tree.Color(hsl, 1, false);
                    }
                },

                keywordcolor: function() {
                    var rgb = chunks[j].match(/^[a-z]+/);
                    if (rgb && rgb[0] in that.env.ref.data.colors) {
                        var data = that.env.ref.data.colors[$(/^[a-z]+/)];
                        var a = 1;
                        if (data.length > 3) {
                            a = data[3];
                        }
                        var hsl = chroma(data.slice(0, 3)).hsl();
                        return new tree.Color(hsl, a, false);
                    }
                },

                // A Dimension, that is, a number and a unit. The only
                // unit that has an effect is %
                dimension: function() {
                    var c = input.charCodeAt(i);
                    if ((c > 57 || c < 45) || c === 47) return;
                    var value = $(/^(-?\d*\.?\d+(?:[eE][-+]?\d+)?)(\%|\w+)?/);
                    if (value) {
                        return new tree.Dimension(value[1], value[2], memo, env.filename);
                    }
                }
            },

            // The variable part of a variable definition.
            // Used in the `rule` parser. Like @fink:
            variable: function() {
                var name;

                if (input.charAt(i) === '@' && (name = $(/^(@[\w-]+)\s*:/))) {
                    return name[1];
                }
            },

            // Entities are the smallest recognized token,
            // and can be found inside a rule's value.
            entity: function() {
                return $(this.entities.call) ||
                    $(this.entities.literal) ||
                    $(this.entities.field) ||
                    $(this.entities.variable) ||
                    $(this.entities.url) ||
                    $(this.entities.keyword);
            },

            // A Rule terminator. Note that we use `peek()` to check for '}',
            // because the `block` rule will be expecting it, but we still need to make sure
            // it's there, if ';' was ommitted.
            end: function() {
                return $(';') || peek('}');
            },

            // Elements are the building blocks for Selectors. They consist of
            // an element name, such as a tag a class, or `*`.
            element: function() {
                var e = $(/^(?:[.#][\w\-]+|\*|Map)/);
                if (e) return new tree.Element(e);
            },

            // Attachments allow adding multiple lines, polygons etc. to an
            // object. There can only be one attachment per selector.
            attachment: function() {
                var s = $(/^::([\w\-]+(?:\/[\w\-]+)*)/);
                if (s) return s[1];
            },

            // Selectors are made out of one or more Elements, see above.
            selector: function() {
                var a, attachment,
                    e, elements = [],
                    f, filters = new tree.Filterset(),
                    z, zooms = [],
                    segments = 0, conditions = 0;

                while (
                        (e = $(this.element)) ||
                        (z = $(this.zoom)) ||
                        (f = $(this.filter)) ||
                        (a = $(this.attachment))
                    ) {
                    segments++;
                    if (e) {
                        elements.push(e);
                    } else if (z) {
                        zooms.push(z);
                        conditions++;
                    } else if (f) {
                        var err = filters.add(f);
                        if (err) {
                            util.error(env, {
                                message: err,
                                index: i - 1,
                                filename: env.filename
                            });
                            throw new Error('N/A');
                        }
                        conditions++;
                    } else if (attachment) {
                        util.error(env, {
                            message: 'Encountered second attachment name.\n',
                            index: i - 1,
                            filename: env.filename
                        });
                        throw new Error('N/A');
                    } else {
                        attachment = a;
                    }

                    var c = input.charAt(i);
                    if (c === '{' || c === '}' || c === ';' || c === ',') { break; }
                }

                if (segments) {
                    return new tree.Selector(filters, zooms, elements, attachment, conditions, memo);
                }
            },

            filter: function() {
                save();
                var key, op, val;
                if (! $('[')) return;

                if ((key = $(/^[a-zA-Z0-9\-_]+/) ||
                    $(this.entities.quoted) ||
                    $(this.expression) ||
                    $(this.entities.variable) ||
                    $(this.entities.keyword) ||
                    $(this.entities.field))) {
                    // TODO: remove at 1.0.0
                    if (key instanceof tree.Quoted) {
                        key = new tree.Field(key.toString());
                    }
                    if ((op = $(this.entities.comparison)) &&
                        (val = $(this.expression) ||
                             $(this.entities.quoted) ||
                             $(this.entities.variable) ||
                             $(this.entities.dimension) ||
                             $(this.entities.keyword) ||
                             $(this.entities.field))) {
                        if (! $(']')) {
                            util.error(env, {
                                message: 'Missing closing ] of filter.',
                                index: memo - 1,
                                filename: env.filename
                            });
                            throw new Error('N/A');
                        }
                        if (!key.is) key = new tree.Field(key);
                        return new tree.Filter(key, op, val, memo, env.filename);
                    }
                }
            },

            zoom: function() {
                save();
                var op, val;
                if ($(/^\[\s*zoom/g) &&
                    (op = $(this.entities.comparison)) &&
                    (val = $(this.entities.variable) || $(this.entities.dimension)) && $(']')) {
                        return new tree.Zoom(op, val, memo, env.filename);
                } else {
                    // backtrack
                    restore();
                }
            },

            // The `block` rule is used by `ruleset`
            // It's a wrapper around the `primary` rule, with added `{}`.
            block: function() {
                var content;

                if ($('{') && (content = $(this.primary)) && $('}')) {
                    return content;
                }
            },

            // div, .class, body > p {...}
            ruleset: function() {
                var selectors = [], s, rules;
                save();

                s = $(this.selector);
                while (s) {
                    selectors.push(s);
                    while ($(this.comment)) {
                        // do nothing
                    }
                    if (! $(',')) { break; }
                    while ($(this.comment)) {
                        // do nothing
                    }
                    s = $(this.selector);
                }
                if (s) {
                    while ($(this.comment)) {
                        // do nothing
                    }
                }

                if (selectors.length > 0 && (rules = $(this.block))) {
                    if (selectors.length === 1 &&
                        selectors[0].elements.length &&
                        selectors[0].elements[0].value === 'Map') {
                        var rs = new tree.Ruleset(selectors, rules);
                        rs.isMap = true;
                        return rs;
                    }
                    return new tree.Ruleset(selectors, rules);
                } else {
                    // Backtrack
                    restore();
                }
            },

            rule: function() {
                var name, value, c = input.charAt(i);
                save();

                if (c === '.' || c === '#') { return; }

                if ((name = $(this.variable) || $(this.property))) {
                    value = $(this.value);

                    if (value && $(this.end)) {
                        return new tree.Rule(env.ref, name, value, memo, env.filename);
                    } else {
                        restore();
                    }
                }
            },

            font: function() {
                var value = [], expression = [], e;

                e = $(this.entity);
                while (e) {
                    expression.push(e);
                    e = $(this.entity);
                }

                value.push(new tree.Expression(expression));

                if ($(',')) {
                    e = $(this.expression);
                    while (e) {
                        value.push(e);
                        if (! $(',')) { break; }
                        e = $(this.expression);
                    }
                }
                return new tree.Value(value);
            },

            // A Value is a comma-delimited list of Expressions
            // In a Rule, a Value represents everything after the `:`,
            // and before the `;`.
            value: function() {
                var e, expressions = [];

                e = $(this.expression);
                while (e) {
                    expressions.push(e);
                    if (! $(',')) { break; }
                    e = $(this.expression);
                }

                if (expressions.length > 1) {
                    return new tree.Value(expressions.map(function(e) {
                        return e.value[0];
                    }));
                } else if (expressions.length === 1) {
                    return new tree.Value(expressions);
                }
            },
            // A sub-expression, contained by parenthensis
            sub: function() {
                var e;

                if ($('(') && (e = $(this.expression)) && $(')')) {
                    return e;
                }
            },
            // This is a misnomer because it actually handles multiplication
            // and division.
            multiplication: function() {
                var m, a, op, operation;
                m = $(this.operand);
                if (m) {
                    while ((op = ($('/') || $('*') || $('%'))) && (a = $(this.operand))) {
                        operation = new tree.Operation(op, [operation || m, a], memo, env.filename);
                    }
                    return operation || m;
                }
            },
            addition: function() {
                var m, a, op, operation;
                m = $(this.multiplication);
                if (m) {
                    while ((op = $(/^[-+]\s+/) || (input.charAt(i - 1) != ' ' && ($('+') || $('-')))) &&
                           (a = $(this.multiplication))) {
                        operation = new tree.Operation(op, [operation || m, a], memo, env.filename);
                    }
                    return operation || m;
                }
            },

            // An operand is anything that can be part of an operation,
            // such as a Color, or a Variable
            operand: function() {
                return $(this.sub) || $(this.entity);
            },

            // Expressions either represent mathematical operations,
            // or white-space delimited Entities.  @var * 2
            expression: function() {
                var e, entities = [];

                e = $(this.addition);
                while (e || $(this.entity)) {
                    entities.push(e);
                    e = $(this.addition);
                }

                if (entities.length > 0) {
                    return new tree.Expression(entities);
                }
            },
            property: function() {
                var name = $(/^(([a-z][-a-z_0-9]*\/)?\*?-?[-a-z_0-9]+)\s*:/);
                if (name) return name[1];
            }
        }
    };
    return parser;
};
