/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';
import * as lessScanner from './lessScanner';
import { TokenType } from './cssScanner';
import * as cssParser from './cssParser';
import * as nodes from './cssNodes';
import { ParseError } from './cssErrors';
/// <summary>
/// A parser for LESS
/// http://lesscss.org/
/// </summary>
export class LESSParser extends cssParser.Parser {
    constructor() {
        super(new lessScanner.LESSScanner());
    }
    _parseStylesheetStatement(isNested = false) {
        if (this.peek(TokenType.AtKeyword)) {
            return this._parseVariableDeclaration()
                || this._parsePlugin()
                || super._parseStylesheetAtStatement(isNested);
        }
        return this._tryParseMixinDeclaration()
            || this._tryParseMixinReference()
            || this._parseFunction()
            || this._parseRuleset(true);
    }
    _parseImport() {
        if (!this.peekKeyword('@import') && !this.peekKeyword('@import-once') /* deprecated in less 1.4.1 */) {
            return null;
        }
        const node = this.create(nodes.Import);
        this.consumeToken();
        // less 1.4.1: @import (css) "lib"
        if (this.accept(TokenType.ParenthesisL)) {
            if (!this.accept(TokenType.Ident)) {
                return this.finish(node, ParseError.IdentifierExpected, [TokenType.SemiColon]);
            }
            do {
                if (!this.accept(TokenType.Comma)) {
                    break;
                }
            } while (this.accept(TokenType.Ident));
            if (!this.accept(TokenType.ParenthesisR)) {
                return this.finish(node, ParseError.RightParenthesisExpected, [TokenType.SemiColon]);
            }
        }
        if (!node.addChild(this._parseURILiteral()) && !node.addChild(this._parseStringLiteral())) {
            return this.finish(node, ParseError.URIOrStringExpected, [TokenType.SemiColon]);
        }
        if (!this.peek(TokenType.SemiColon) && !this.peek(TokenType.EOF)) {
            node.setMedialist(this._parseMediaQueryList());
        }
        return this.finish(node);
    }
    _parsePlugin() {
        if (!this.peekKeyword('@plugin')) {
            return null;
        }
        const node = this.createNode(nodes.NodeType.Plugin);
        this.consumeToken(); // @import
        if (!node.addChild(this._parseStringLiteral())) {
            return this.finish(node, ParseError.StringLiteralExpected);
        }
        if (!this.accept(TokenType.SemiColon)) {
            return this.finish(node, ParseError.SemiColonExpected);
        }
        return this.finish(node);
    }
    _parseMediaQuery() {
        const node = super._parseMediaQuery();
        if (!node) {
            const node = this.create(nodes.MediaQuery);
            if (node.addChild(this._parseVariable())) {
                return this.finish(node);
            }
            return null;
        }
        return node;
    }
    _parseMediaDeclaration(isNested = false) {
        return this._tryParseRuleset(isNested)
            || this._tryToParseDeclaration()
            || this._tryParseMixinDeclaration()
            || this._tryParseMixinReference()
            || this._parseDetachedRuleSetMixin()
            || this._parseStylesheetStatement(isNested);
    }
    _parseMediaFeatureName() {
        return this._parseIdent() || this._parseVariable();
    }
    _parseVariableDeclaration(panic = []) {
        const node = this.create(nodes.VariableDeclaration);
        const mark = this.mark();
        if (!node.setVariable(this._parseVariable(true))) {
            return null;
        }
        if (this.accept(TokenType.Colon)) {
            if (this.prevToken) {
                node.colonPosition = this.prevToken.offset;
            }
            if (node.setValue(this._parseDetachedRuleSet())) {
                node.needsSemicolon = false;
            }
            else if (!node.setValue(this._parseExpr())) {
                return this.finish(node, ParseError.VariableValueExpected, [], panic);
            }
            node.addChild(this._parsePrio());
        }
        else {
            this.restoreAtMark(mark);
            return null; // at keyword, but no ':', not a variable declaration but some at keyword
        }
        if (this.peek(TokenType.SemiColon)) {
            node.semicolonPosition = this.token.offset; // not part of the declaration, but useful information for code assist
        }
        return this.finish(node);
    }
    _parseDetachedRuleSet() {
        let mark = this.mark();
        // "Anonymous mixin" used in each() and possibly a generic type in the future
        if (this.peekDelim('#') || this.peekDelim('.')) {
            this.consumeToken();
            if (!this.hasWhitespace() && this.accept(TokenType.ParenthesisL)) {
                let node = this.create(nodes.MixinDeclaration);
                if (node.getParameters().addChild(this._parseMixinParameter())) {
                    while (this.accept(TokenType.Comma) || this.accept(TokenType.SemiColon)) {
                        if (this.peek(TokenType.ParenthesisR)) {
                            break;
                        }
                        if (!node.getParameters().addChild(this._parseMixinParameter())) {
                            this.markError(node, ParseError.IdentifierExpected, [], [TokenType.ParenthesisR]);
                        }
                    }
                }
                if (!this.accept(TokenType.ParenthesisR)) {
                    this.restoreAtMark(mark);
                    return null;
                }
            }
            else {
                this.restoreAtMark(mark);
                return null;
            }
        }
        if (!this.peek(TokenType.CurlyL)) {
            return null;
        }
        const content = this.create(nodes.BodyDeclaration);
        this._parseBody(content, this._parseDetachedRuleSetBody.bind(this));
        return this.finish(content);
    }
    _parseDetachedRuleSetBody() {
        return this._tryParseKeyframeSelector() || this._parseRuleSetDeclaration();
    }
    _addLookupChildren(node) {
        if (!node.addChild(this._parseLookupValue())) {
            return false;
        }
        let expectsValue = false;
        while (true) {
            if (this.peek(TokenType.BracketL)) {
                expectsValue = true;
            }
            if (!node.addChild(this._parseLookupValue())) {
                break;
            }
            expectsValue = false;
        }
        return !expectsValue;
    }
    _parseLookupValue() {
        const node = this.create(nodes.Node);
        const mark = this.mark();
        if (!this.accept(TokenType.BracketL)) {
            this.restoreAtMark(mark);
            return null;
        }
        if (((node.addChild(this._parseVariable(false, true)) ||
            node.addChild(this._parsePropertyIdentifier())) &&
            this.accept(TokenType.BracketR)) || this.accept(TokenType.BracketR)) {
            return node;
        }
        this.restoreAtMark(mark);
        return null;
    }
    _parseVariable(declaration = false, insideLookup = false) {
        const isPropertyReference = !declaration && this.peekDelim('$');
        if (!this.peekDelim('@') && !isPropertyReference && !this.peek(TokenType.AtKeyword)) {
            return null;
        }
        const node = this.create(nodes.Variable);
        const mark = this.mark();
        while (this.acceptDelim('@') || (!declaration && this.acceptDelim('$'))) {
            if (this.hasWhitespace()) {
                this.restoreAtMark(mark);
                return null;
            }
        }
        if (!this.accept(TokenType.AtKeyword) && !this.accept(TokenType.Ident)) {
            this.restoreAtMark(mark);
            return null;
        }
        if (!insideLookup && this.peek(TokenType.BracketL)) {
            if (!this._addLookupChildren(node)) {
                this.restoreAtMark(mark);
                return null;
            }
        }
        return node;
    }
    _parseTermExpression() {
        return this._parseVariable() ||
            this._parseEscaped() ||
            super._parseTermExpression() || // preference for colors before mixin references
            this._tryParseMixinReference(false);
    }
    _parseEscaped() {
        if (this.peek(TokenType.EscapedJavaScript) ||
            this.peek(TokenType.BadEscapedJavaScript)) {
            const node = this.createNode(nodes.NodeType.EscapedValue);
            this.consumeToken();
            return this.finish(node);
        }
        if (this.peekDelim('~')) {
            const node = this.createNode(nodes.NodeType.EscapedValue);
            this.consumeToken();
            if (this.accept(TokenType.String) || this.accept(TokenType.EscapedJavaScript)) {
                return this.finish(node);
            }
            else {
                return this.finish(node, ParseError.TermExpected);
            }
        }
        return null;
    }
    _parseOperator() {
        const node = this._parseGuardOperator();
        if (node) {
            return node;
        }
        else {
            return super._parseOperator();
        }
    }
    _parseGuardOperator() {
        if (this.peekDelim('>')) {
            const node = this.createNode(nodes.NodeType.Operator);
            this.consumeToken();
            this.acceptDelim('=');
            return node;
        }
        else if (this.peekDelim('=')) {
            const node = this.createNode(nodes.NodeType.Operator);
            this.consumeToken();
            this.acceptDelim('<');
            return node;
        }
        else if (this.peekDelim('<')) {
            const node = this.createNode(nodes.NodeType.Operator);
            this.consumeToken();
            this.acceptDelim('=');
            return node;
        }
        return null;
    }
    _parseRuleSetDeclaration() {
        if (this.peek(TokenType.AtKeyword)) {
            return this._parseKeyframe()
                || this._parseMedia(true)
                || this._parseImport()
                || this._parseSupports(true) // @supports
                || this._parseDetachedRuleSetMixin() // less detached ruleset mixin
                || this._parseVariableDeclaration() // Variable declarations
                || super._parseRuleSetDeclarationAtStatement();
        }
        return this._tryParseMixinDeclaration()
            || this._tryParseRuleset(true) // nested ruleset
            || this._tryParseMixinReference() // less mixin reference
            || this._parseFunction()
            || this._parseExtend() // less extend declaration
            || super._parseRuleSetDeclaration(); // try css ruleset declaration as the last option
    }
    _parseKeyframeIdent() {
        return this._parseIdent([nodes.ReferenceType.Keyframe]) || this._parseVariable();
    }
    _parseKeyframeSelector() {
        return this._parseDetachedRuleSetMixin() // less detached ruleset mixin
            || super._parseKeyframeSelector();
    }
    _parseSimpleSelectorBody() {
        return this._parseSelectorCombinator() || super._parseSimpleSelectorBody();
    }
    _parseSelector(isNested) {
        // CSS Guards
        const node = this.create(nodes.Selector);
        let hasContent = false;
        if (isNested) {
            // nested selectors can start with a combinator
            hasContent = node.addChild(this._parseCombinator());
        }
        while (node.addChild(this._parseSimpleSelector())) {
            hasContent = true;
            const mark = this.mark();
            if (node.addChild(this._parseGuard()) && this.peek(TokenType.CurlyL)) {
                break;
            }
            this.restoreAtMark(mark);
            node.addChild(this._parseCombinator()); // optional
        }
        return hasContent ? this.finish(node) : null;
    }
    _parseSelectorCombinator() {
        if (this.peekDelim('&')) {
            const node = this.createNode(nodes.NodeType.SelectorCombinator);
            this.consumeToken();
            while (!this.hasWhitespace() && (this.acceptDelim('-') || this.accept(TokenType.Num) || this.accept(TokenType.Dimension) || node.addChild(this._parseIdent()) || this.acceptDelim('&'))) {
                //  support &-foo
            }
            return this.finish(node);
        }
        return null;
    }
    _parseSelectorIdent() {
        if (!this.peekInterpolatedIdent()) {
            return null;
        }
        const node = this.createNode(nodes.NodeType.SelectorInterpolation);
        const hasContent = this._acceptInterpolatedIdent(node);
        return hasContent ? this.finish(node) : null;
    }
    _parsePropertyIdentifier(inLookup = false) {
        const propertyRegex = /^[\w-]+/;
        if (!this.peekInterpolatedIdent() && !this.peekRegExp(this.token.type, propertyRegex)) {
            return null;
        }
        const mark = this.mark();
        const node = this.create(nodes.Identifier);
        node.isCustomProperty = this.acceptDelim('-') && this.acceptDelim('-');
        let childAdded = false;
        if (!inLookup) {
            if (node.isCustomProperty) {
                childAdded = this._acceptInterpolatedIdent(node);
            }
            else {
                childAdded = this._acceptInterpolatedIdent(node, propertyRegex);
            }
        }
        else {
            if (node.isCustomProperty) {
                childAdded = node.addChild(this._parseIdent());
            }
            else {
                childAdded = node.addChild(this._parseRegexp(propertyRegex));
            }
        }
        if (!childAdded) {
            this.restoreAtMark(mark);
            return null;
        }
        if (!inLookup && !this.hasWhitespace()) {
            this.acceptDelim('+');
            if (!this.hasWhitespace()) {
                this.acceptIdent('_');
            }
        }
        return this.finish(node);
    }
    peekInterpolatedIdent() {
        return this.peek(TokenType.Ident) ||
            this.peekDelim('@') ||
            this.peekDelim('$') ||
            this.peekDelim('-');
    }
    _acceptInterpolatedIdent(node, identRegex) {
        let hasContent = false;
        const indentInterpolation = () => {
            const pos = this.mark();
            if (this.acceptDelim('-')) {
                if (!this.hasWhitespace()) {
                    this.acceptDelim('-');
                }
                if (this.hasWhitespace()) {
                    this.restoreAtMark(pos);
                    return null;
                }
            }
            return this._parseInterpolation();
        };
        const accept = identRegex ?
            () => this.acceptRegexp(identRegex) :
            () => this.accept(TokenType.Ident);
        while (accept() ||
            node.addChild(this._parseInterpolation() ||
                this.try(indentInterpolation))) {
            hasContent = true;
            if (this.hasWhitespace()) {
                break;
            }
        }
        return hasContent;
    }
    _parseInterpolation() {
        // @{name} Variable or
        // ${name} Property
        const mark = this.mark();
        if (this.peekDelim('@') || this.peekDelim('$')) {
            const node = this.createNode(nodes.NodeType.Interpolation);
            this.consumeToken();
            if (this.hasWhitespace() || !this.accept(TokenType.CurlyL)) {
                this.restoreAtMark(mark);
                return null;
            }
            if (!node.addChild(this._parseIdent())) {
                return this.finish(node, ParseError.IdentifierExpected);
            }
            if (!this.accept(TokenType.CurlyR)) {
                return this.finish(node, ParseError.RightCurlyExpected);
            }
            return this.finish(node);
        }
        return null;
    }
    _tryParseMixinDeclaration() {
        const mark = this.mark();
        const node = this.create(nodes.MixinDeclaration);
        if (!node.setIdentifier(this._parseMixinDeclarationIdentifier()) || !this.accept(TokenType.ParenthesisL)) {
            this.restoreAtMark(mark);
            return null;
        }
        if (node.getParameters().addChild(this._parseMixinParameter())) {
            while (this.accept(TokenType.Comma) || this.accept(TokenType.SemiColon)) {
                if (this.peek(TokenType.ParenthesisR)) {
                    break;
                }
                if (!node.getParameters().addChild(this._parseMixinParameter())) {
                    this.markError(node, ParseError.IdentifierExpected, [], [TokenType.ParenthesisR]);
                }
            }
        }
        if (!this.accept(TokenType.ParenthesisR)) {
            this.restoreAtMark(mark);
            return null;
        }
        node.setGuard(this._parseGuard());
        if (!this.peek(TokenType.CurlyL)) {
            this.restoreAtMark(mark);
            return null;
        }
        return this._parseBody(node, this._parseMixInBodyDeclaration.bind(this));
    }
    _parseMixInBodyDeclaration() {
        return this._parseFontFace() || this._parseRuleSetDeclaration();
    }
    _parseMixinDeclarationIdentifier() {
        let identifier;
        if (this.peekDelim('#') || this.peekDelim('.')) {
            identifier = this.create(nodes.Identifier);
            this.consumeToken(); // # or .
            if (this.hasWhitespace() || !identifier.addChild(this._parseIdent())) {
                return null;
            }
        }
        else if (this.peek(TokenType.Hash)) {
            identifier = this.create(nodes.Identifier);
            this.consumeToken(); // TokenType.Hash
        }
        else {
            return null;
        }
        identifier.referenceTypes = [nodes.ReferenceType.Mixin];
        return this.finish(identifier);
    }
    _parsePseudo() {
        if (!this.peek(TokenType.Colon)) {
            return null;
        }
        const mark = this.mark();
        const node = this.create(nodes.ExtendsReference);
        this.consumeToken(); // :
        if (this.acceptIdent('extend')) {
            return this._completeExtends(node);
        }
        this.restoreAtMark(mark);
        return super._parsePseudo();
    }
    _parseExtend() {
        if (!this.peekDelim('&')) {
            return null;
        }
        const mark = this.mark();
        const node = this.create(nodes.ExtendsReference);
        this.consumeToken(); // &
        if (this.hasWhitespace() || !this.accept(TokenType.Colon) || !this.acceptIdent('extend')) {
            this.restoreAtMark(mark);
            return null;
        }
        return this._completeExtends(node);
    }
    _completeExtends(node) {
        if (!this.accept(TokenType.ParenthesisL)) {
            return this.finish(node, ParseError.LeftParenthesisExpected);
        }
        const selectors = node.getSelectors();
        if (!selectors.addChild(this._parseSelector(true))) {
            return this.finish(node, ParseError.SelectorExpected);
        }
        while (this.accept(TokenType.Comma)) {
            if (!selectors.addChild(this._parseSelector(true))) {
                return this.finish(node, ParseError.SelectorExpected);
            }
        }
        if (!this.accept(TokenType.ParenthesisR)) {
            return this.finish(node, ParseError.RightParenthesisExpected);
        }
        return this.finish(node);
    }
    _parseDetachedRuleSetMixin() {
        if (!this.peek(TokenType.AtKeyword)) {
            return null;
        }
        const mark = this.mark();
        const node = this.create(nodes.MixinReference);
        if (node.addChild(this._parseVariable(true)) && (this.hasWhitespace() || !this.accept(TokenType.ParenthesisL))) {
            this.restoreAtMark(mark);
            return null;
        }
        if (!this.accept(TokenType.ParenthesisR)) {
            return this.finish(node, ParseError.RightParenthesisExpected);
        }
        return this.finish(node);
    }
    _tryParseMixinReference(atRoot = true) {
        const mark = this.mark();
        const node = this.create(nodes.MixinReference);
        let identifier = this._parseMixinDeclarationIdentifier();
        while (identifier) {
            this.acceptDelim('>');
            const nextId = this._parseMixinDeclarationIdentifier();
            if (nextId) {
                node.getNamespaces().addChild(identifier);
                identifier = nextId;
            }
            else {
                break;
            }
        }
        if (!node.setIdentifier(identifier)) {
            this.restoreAtMark(mark);
            return null;
        }
        let hasArguments = false;
        if (this.accept(TokenType.ParenthesisL)) {
            hasArguments = true;
            if (node.getArguments().addChild(this._parseMixinArgument())) {
                while (this.accept(TokenType.Comma) || this.accept(TokenType.SemiColon)) {
                    if (this.peek(TokenType.ParenthesisR)) {
                        break;
                    }
                    if (!node.getArguments().addChild(this._parseMixinArgument())) {
                        return this.finish(node, ParseError.ExpressionExpected);
                    }
                }
            }
            if (!this.accept(TokenType.ParenthesisR)) {
                return this.finish(node, ParseError.RightParenthesisExpected);
            }
            identifier.referenceTypes = [nodes.ReferenceType.Mixin];
        }
        else {
            identifier.referenceTypes = [nodes.ReferenceType.Mixin, nodes.ReferenceType.Rule];
        }
        if (this.peek(TokenType.BracketL)) {
            if (!atRoot) {
                this._addLookupChildren(node);
            }
        }
        else {
            node.addChild(this._parsePrio());
        }
        if (!hasArguments && !this.peek(TokenType.SemiColon) && !this.peek(TokenType.CurlyR) && !this.peek(TokenType.EOF)) {
            this.restoreAtMark(mark);
            return null;
        }
        return this.finish(node);
    }
    _parseMixinArgument() {
        // [variableName ':'] expression | variableName '...'
        const node = this.create(nodes.FunctionArgument);
        const pos = this.mark();
        const argument = this._parseVariable();
        if (argument) {
            if (!this.accept(TokenType.Colon)) {
                this.restoreAtMark(pos);
            }
            else {
                node.setIdentifier(argument);
            }
        }
        if (node.setValue(this._parseDetachedRuleSet() || this._parseExpr(true))) {
            return this.finish(node);
        }
        this.restoreAtMark(pos);
        return null;
    }
    _parseMixinParameter() {
        const node = this.create(nodes.FunctionParameter);
        // special rest variable: @rest...
        if (this.peekKeyword('@rest')) {
            const restNode = this.create(nodes.Node);
            this.consumeToken();
            if (!this.accept(lessScanner.Ellipsis)) {
                return this.finish(node, ParseError.DotExpected, [], [TokenType.Comma, TokenType.ParenthesisR]);
            }
            node.setIdentifier(this.finish(restNode));
            return this.finish(node);
        }
        // special const args: ...
        if (this.peek(lessScanner.Ellipsis)) {
            const varargsNode = this.create(nodes.Node);
            this.consumeToken();
            node.setIdentifier(this.finish(varargsNode));
            return this.finish(node);
        }
        let hasContent = false;
        // default variable declaration: @param: 12 or @name
        if (node.setIdentifier(this._parseVariable())) {
            this.accept(TokenType.Colon);
            hasContent = true;
        }
        if (!node.setDefaultValue(this._parseDetachedRuleSet() || this._parseExpr(true)) && !hasContent) {
            return null;
        }
        return this.finish(node);
    }
    _parseGuard() {
        if (!this.peekIdent('when')) {
            return null;
        }
        const node = this.create(nodes.LessGuard);
        this.consumeToken(); // when
        node.isNegated = this.acceptIdent('not');
        if (!node.getConditions().addChild(this._parseGuardCondition())) {
            return this.finish(node, ParseError.ConditionExpected);
        }
        while (this.acceptIdent('and') || this.accept(TokenType.Comma)) {
            if (!node.getConditions().addChild(this._parseGuardCondition())) {
                return this.finish(node, ParseError.ConditionExpected);
            }
        }
        return this.finish(node);
    }
    _parseGuardCondition() {
        if (!this.peek(TokenType.ParenthesisL)) {
            return null;
        }
        const node = this.create(nodes.GuardCondition);
        this.consumeToken(); // ParenthesisL
        if (!node.addChild(this._parseExpr())) {
            // empty (?)
        }
        if (!this.accept(TokenType.ParenthesisR)) {
            return this.finish(node, ParseError.RightParenthesisExpected);
        }
        return this.finish(node);
    }
    _parseFunction() {
        const pos = this.mark();
        const node = this.create(nodes.Function);
        if (!node.setIdentifier(this._parseFunctionIdentifier())) {
            return null;
        }
        if (this.hasWhitespace() || !this.accept(TokenType.ParenthesisL)) {
            this.restoreAtMark(pos);
            return null;
        }
        if (node.getArguments().addChild(this._parseMixinArgument())) {
            while (this.accept(TokenType.Comma) || this.accept(TokenType.SemiColon)) {
                if (this.peek(TokenType.ParenthesisR)) {
                    break;
                }
                if (!node.getArguments().addChild(this._parseMixinArgument())) {
                    return this.finish(node, ParseError.ExpressionExpected);
                }
            }
        }
        if (!this.accept(TokenType.ParenthesisR)) {
            return this.finish(node, ParseError.RightParenthesisExpected);
        }
        return this.finish(node);
    }
    _parseFunctionIdentifier() {
        if (this.peekDelim('%')) {
            const node = this.create(nodes.Identifier);
            node.referenceTypes = [nodes.ReferenceType.Function];
            this.consumeToken();
            return this.finish(node);
        }
        return super._parseFunctionIdentifier();
    }
    _parseURLArgument() {
        const pos = this.mark();
        const node = super._parseURLArgument();
        if (!node || !this.peek(TokenType.ParenthesisR)) {
            this.restoreAtMark(pos);
            const node = this.create(nodes.Node);
            node.addChild(this._parseBinaryExpr());
            return this.finish(node);
        }
        return node;
    }
}
