'use strict';

var names = require('../utils/names');
var MULTIPLIER_DEFAULT = {
    comma: false,
    min: 1,
    max: 1
};

function skipSpaces(node) {
    while (node !== null && (node.data.type === 'WhiteSpace' || node.data.type === 'Comment')) {
        node = node.next;
    }

    return node;
}

function putResult(buffer, match) {
    var type = match.type || match.syntax.type;

    // ignore groups
    if (type === 'Group') {
        buffer.push.apply(buffer, match.match);
    } else {
        buffer.push(match);
    }
}

function matchToJSON() {
    return {
        type: this.syntax.type,
        name: this.syntax.name,
        match: this.match,
        node: this.node
    };
}

function buildMatchNode(badNode, lastNode, next, match) {
    if (badNode) {
        return {
            badNode: badNode,
            lastNode: null,
            next: null,
            match: null
        };
    }

    return {
        badNode: null,
        lastNode: lastNode,
        next: next,
        match: match
    };
}

function matchGroup(lexer, syntaxNode, node) {
    var result = [];
    var buffer;
    var multiplier = syntaxNode.multiplier || MULTIPLIER_DEFAULT;
    var min = multiplier.min;
    var max = multiplier.max === 0 ? Infinity : multiplier.max;
    var lastCommaTermCount;
    var lastComma;
    var matchCount = 0;
    var lastNode = null;
    var badNode = null;

    mismatch:
    while (matchCount < max) {
        node = skipSpaces(node);
        buffer = [];

        switch (syntaxNode.combinator) {
            case '|':
                for (var i = 0; i < syntaxNode.terms.length; i++) {
                    var term = syntaxNode.terms[i];
                    var res = matchSyntax(lexer, term, node);

                    if (res.match) {
                        putResult(buffer, res.match);
                        node = res.next;
                        break;  // continue matching
                    } else if (res.badNode) {
                        badNode = res.badNode;
                        break mismatch;
                    } else if (res.lastNode) {
                        lastNode = res.lastNode;
                    }
                }

                if (buffer.length === 0) {
                    break mismatch; // nothing found -> stop matching
                }

                break;

            case ' ':
                var beforeMatchNode = node;
                var lastMatchedTerm = null;
                var hasTailMatch = false;
                var commaMissed = false;

                for (var i = 0; i < syntaxNode.terms.length; i++) {
                    var term = syntaxNode.terms[i];
                    var res = matchSyntax(lexer, term, node);

                    if (res.match) {
                        if (term.type === 'Comma' && i !== 0 && !hasTailMatch) {
                            // recover cursor to state before last match and stop matching
                            lastNode = node && node.data;
                            node = beforeMatchNode;
                            break mismatch;
                        }

                        // non-empty match (res.next will refer to another node)
                        if (res.next !== node) {
                            // match should be preceded by a comma
                            if (commaMissed) {
                                lastNode = node && node.data;
                                node = beforeMatchNode;
                                break mismatch;
                            }

                            hasTailMatch = term.type !== 'Comma';
                            lastMatchedTerm = term;
                        }

                        putResult(buffer, res.match);
                        node = skipSpaces(res.next);
                    } else if (res.badNode) {
                        badNode = res.badNode;
                        break mismatch;
                    } else {
                        if (res.lastNode) {
                            lastNode = res.lastNode;
                        }

                        // it's ok when comma doesn't match when no matches yet
                        // but only if comma is not first or last term
                        if (term.type === 'Comma' && i !== 0 && i !== syntaxNode.terms.length - 1) {
                            if (hasTailMatch) {
                                commaMissed = true;
                            }
                            continue;
                        }

                        // recover cursor to state before last match and stop matching
                        lastNode = res.lastNode || (node && node.data);
                        node = beforeMatchNode;
                        break mismatch;
                    }
                }

                // don't allow empty match when [ ]!
                if (!lastMatchedTerm && syntaxNode.disallowEmpty) {
                    // empty match but shouldn't
                    // recover cursor to state before last match and stop matching
                    lastNode = node && node.data;
                    node = beforeMatchNode;
                    break mismatch;
                }

                // don't allow comma at the end but only if last term isn't a comma
                if (lastMatchedTerm && lastMatchedTerm.type === 'Comma' && term.type !== 'Comma') {
                    lastNode = node && node.data;
                    node = beforeMatchNode;
                    break mismatch;
                }

                break;

            case '&&':
                var beforeMatchNode = node;
                var lastMatchedTerm = null;
                var terms = syntaxNode.terms.slice();

                while (terms.length) {
                    var wasMatch = false;
                    var emptyMatched = 0;

                    for (var i = 0; i < terms.length; i++) {
                        var term = terms[i];
                        var res = matchSyntax(lexer, term, node);

                        if (res.match) {
                            // non-empty match (res.next will refer to another node)
                            if (res.next !== node) {
                                lastMatchedTerm = term;
                            } else {
                                emptyMatched++;
                                continue;
                            }

                            wasMatch = true;
                            terms.splice(i--, 1);
                            putResult(buffer, res.match);
                            node = skipSpaces(res.next);
                            break;
                        } else if (res.badNode) {
                            badNode = res.badNode;
                            break mismatch;
                        } else if (res.lastNode) {
                            lastNode = res.lastNode;
                        }
                    }

                    if (!wasMatch) {
                        // terms left, but they all are optional
                        if (emptyMatched === terms.length) {
                            break;
                        }

                        // not ok
                        lastNode = node && node.data;
                        node = beforeMatchNode;
                        break mismatch;
                    }
                }

                if (!lastMatchedTerm && syntaxNode.disallowEmpty) { // don't allow empty match when [ ]!
                    // empty match but shouldn't
                    // recover cursor to state before last match and stop matching
                    lastNode = node && node.data;
                    node = beforeMatchNode;
                    break mismatch;
                }

                break;

            case '||':
                var beforeMatchNode = node;
                var lastMatchedTerm = null;
                var terms = syntaxNode.terms.slice();

                while (terms.length) {
                    var wasMatch = false;
                    var emptyMatched = 0;

                    for (var i = 0; i < terms.length; i++) {
                        var term = terms[i];
                        var res = matchSyntax(lexer, term, node);

                        if (res.match) {
                            // non-empty match (res.next will refer to another node)
                            if (res.next !== node) {
                                lastMatchedTerm = term;
                            } else {
                                emptyMatched++;
                                continue;
                            }

                            wasMatch = true;
                            terms.splice(i--, 1);
                            putResult(buffer, res.match);
                            node = skipSpaces(res.next);
                            break;
                        } else if (res.badNode) {
                            badNode = res.badNode;
                            break mismatch;
                        } else if (res.lastNode) {
                            lastNode = res.lastNode;
                        }
                    }

                    if (!wasMatch) {
                        break;
                    }
                }

                // don't allow empty match
                if (!lastMatchedTerm && (emptyMatched !== terms.length || syntaxNode.disallowEmpty)) {
                    // empty match but shouldn't
                    // recover cursor to state before last match and stop matching
                    lastNode = node && node.data;
                    node = beforeMatchNode;
                    break mismatch;
                }

                break;
        }

        // flush buffer
        result.push.apply(result, buffer);
        matchCount++;

        if (!node) {
            break;
        }

        if (multiplier.comma) {
            if (lastComma && lastCommaTermCount === result.length) {
                // nothing match after comma
                break mismatch;
            }

            node = skipSpaces(node);
            if (node !== null && node.data.type === 'Operator' && node.data.value === ',') {
                result.push({
                    syntax: syntaxNode,
                    match: [{
                        type: 'ASTNode',
                        node: node.data,
                        childrenMatch: null
                    }]
                });
                lastCommaTermCount = result.length;
                lastComma = node;
                node = node.next;
            } else {
                lastNode = node !== null ? node.data : null;
                break mismatch;
            }
        }
    }

    // console.log(syntaxNode.type, badNode, lastNode);

    if (lastComma && lastCommaTermCount === result.length) {
        // nothing match after comma
        node = lastComma;
        result.pop();
    }

    return buildMatchNode(badNode, lastNode, node, matchCount < min ? null : {
        syntax: syntaxNode,
        match: result,
        toJSON: matchToJSON
    });
}

function matchSyntax(lexer, syntaxNode, node) {
    var badNode = null;
    var lastNode = null;
    var match = null;

    switch (syntaxNode.type) {
        case 'Group':
            return matchGroup(lexer, syntaxNode, node);

        case 'Function':
            // expect a function node
            if (!node || node.data.type !== 'Function') {
                break;
            }

            var keyword = names.keyword(node.data.name);
            var name = syntaxNode.name.toLowerCase();

            // check function name with vendor consideration
            if (name !== keyword.vendor + keyword.name) {
                break;
            }

            var res = matchSyntax(lexer, syntaxNode.children, node.data.children.head);
            if (!res.match || res.next) {
                badNode = res.badNode || res.lastNode || (res.next ? res.next.data : null) || node.data;
                break;
            }

            match = [{
                type: 'ASTNode',
                node: node.data,
                childrenMatch: res.match.match
            }];

            // Use node.next instead of res.next here since syntax is matching
            // for internal list and it should be completelly matched (res.next is null at this point).
            // Therefore function is matched and we are going to next node
            node = node.next;
            break;

        case 'Parentheses':
            if (!node || node.data.type !== 'Parentheses') {
                break;
            }

            var res = matchSyntax(lexer, syntaxNode.children, node.data.children.head);
            if (!res.match || res.next) {
                badNode = res.badNode || res.lastNode || (res.next ? res.next.data : null) || node.data;  // TODO: case when res.next === null
                break;
            }

            match = [{
                type: 'ASTNode',
                node: node.data,
                childrenMatch: res.match.match
            }];

            node = res.next;
            break;

        case 'Type':
            var typeSyntax = lexer.getType(syntaxNode.name);
            if (!typeSyntax) {
                throw new Error('Unknown syntax type `' + syntaxNode.name + '`');
            }

            var res = typeSyntax.match(node);
            if (!res.match) {
                badNode = res && res.badNode; // TODO: case when res.next === null
                lastNode = (res && res.lastNode) || (node && node.data);
                break;
            }

            node = res.next;
            putResult(match = [], res.match);
            if (match.length === 0) {
                match = null;
            }
            break;

        case 'Property':
            var propertySyntax = lexer.getProperty(syntaxNode.name);
            if (!propertySyntax) {
                throw new Error('Unknown property `' + syntaxNode.name + '`');
            }

            var res = propertySyntax.match(node);
            if (!res.match) {
                badNode = res && res.badNode; // TODO: case when res.next === null
                lastNode = (res && res.lastNode) || (node && node.data);
                break;
            }

            node = res.next;
            putResult(match = [], res.match);
            if (match.length === 0) {
                match = null;
            }
            break;

        case 'Keyword':
            if (!node) {
                break;
            }

            if (node.data.type === 'Identifier') {
                var keyword = names.keyword(node.data.name);
                var keywordName = keyword.name;
                var name = syntaxNode.name.toLowerCase();

                // drop \0 and \9 hack from keyword name
                if (keywordName.indexOf('\\') !== -1) {
                    keywordName = keywordName.replace(/\\[09].*$/, '');
                }

                if (name !== keyword.vendor + keywordName) {
                    break;
                }
            } else {
                // keyword may to be a number (e.g. font-weight: 400 )
                if (node.data.type !== 'Number' || node.data.value !== syntaxNode.name) {
                    break;
                }
            }

            match = [{
                type: 'ASTNode',
                node: node.data,
                childrenMatch: null
            }];
            node = node.next;
            break;

        case 'Slash':
        case 'Comma':
            if (!node || node.data.type !== 'Operator' || node.data.value !== syntaxNode.value) {
                break;
            }

            match = [{
                type: 'ASTNode',
                node: node.data,
                childrenMatch: null
            }];
            node = node.next;
            break;

        case 'String':
            if (!node || node.data.type !== 'String') {
                break;
            }

            match = [{
                type: 'ASTNode',
                node: node.data,
                childrenMatch: null
            }];
            node = node.next;
            break;

        case 'ASTNode':
            if (node && syntaxNode.match(node)) {
                match = {
                    type: 'ASTNode',
                    node: node.data,
                    childrenMatch: null
                };
                node = node.next;
            }
            return buildMatchNode(badNode, lastNode, node, match);

        default:
            throw new Error('Not implemented yet node type: ' + syntaxNode.type);
    }

    return buildMatchNode(badNode, lastNode, node, match === null ? null : {
        syntax: syntaxNode,
        match: match,
        toJSON: matchToJSON
    });

};

module.exports = matchSyntax;
