/*
 * Copyright (C) 2019 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

CSSFormatter = class CSSFormatter
{
    constructor(sourceText, builder, indentString = "    ")
    {
        this._success = false;

        this._sourceText = sourceText;

        this._builder = builder;
        if (!this._builder) {
            this._builder = new FormatterContentBuilder(indentString);
            this._builder.setOriginalLineEndings(this._sourceText.lineEndings());
        }

        this._format();

        this._success = true;
    }

    // Public

    get success() { return this._success; }

    get formattedText()
    {
        if (!this._success)
            return null;
        return this._builder.formattedContent;
    }

    get sourceMapData()
    {
        if (!this._success)
            return null;
        return this._builder.sourceMapData;
    }

    // Private

    _format()
    {
        const quoteTypes = new Set([`"`, `'`]);

        const dedentBefore = new Set([`}`]);

        const newlineBefore = new Set([`}`]);
        const newlineAfter = new Set([`{`, `}`, `;`]);

        const indentAfter = new Set([`{`]);

        const addSpaceBefore = new Set([`+`, `*`, `~`, `>`, `(`, `{`, `!`]);
        const addSpaceAfter = new Set([`,`, `+`, `*`, `~`, `>`, `)`, `:`]);

        const removeSpaceBefore = new Set([`,`, `(`, `)`, `}`, `:`, `;`]);
        const removeSpaceAfter = new Set([`(`, `{`, `}`, `,`, `!`, `;`]);

        const whitespaceOnlyRegExp = /^\s*$/;

        const inAtRuleRegExp = /^\s*@[a-zA-Z][-a-zA-Z]+/;
        const inAtRuleBeforeParenthesisRegExp = /^\s*@[a-zA-Z][-a-zA-Z]+$/;
        const inAtRuleAfterParenthesisRegExp = /^\s*@[a-zA-Z][-a-zA-Z]+[^("':]*\([^"':]*:/;
        const inAtSupportsRuleRegExp = /^\s*@[a-zA-Z][-a-zA-Z]+[^"':]*:/;

        const lineStartCouldBePropertyRegExp = /^\s+[-_a-zA-Z][-_a-zA-Z0-9]*/;

        const lastTokenWasOpenParenthesisRegExp = /\(\s*$/;

        let depth = 0;
        let specialSequenceStack = [];

        let index = 0;
        let current = null;

        let testCurrentLine = (regExp) => regExp.test(this._builder.currentLine);

        let inSelector = () => {
            let nextOpenBrace = this._sourceText.indexOf(`{`, index);
            if (nextOpenBrace !== -1) {
                let nextQuote = Infinity;
                for (let quoteType of quoteTypes) {
                    let quoteIndex = this._sourceText.indexOf(quoteType, index);
                    if (quoteIndex !== -1 && quoteIndex < nextQuote)
                        nextQuote = quoteIndex;
                }
                if (nextOpenBrace < nextQuote) {
                    let nextSemicolon = this._sourceText.indexOf(`;`, index);
                    if (nextSemicolon === -1)
                        nextSemicolon = Infinity;

                    let nextNewline = this._sourceText.indexOf(`\n`, index);
                    if (nextNewline === -1)
                        nextNewline = Infinity;

                    if (nextOpenBrace < Math.min(nextSemicolon, nextNewline))
                        return true;
                }
            }

            if (testCurrentLine(lineStartCouldBePropertyRegExp))
                return false;

            return true;
        };

        let inProperty = () => {
            if (!depth)
                return false;
            return !testCurrentLine(inAtRuleRegExp) && !inSelector();
        };

        let formatBefore = () => {
            if (this._builder.lastNewlineAppendWasMultiple && current === `}`)
                this._builder.removeLastNewline();

            if (dedentBefore.has(current))
                this._builder.dedent();

            if (!this._builder.lastTokenWasNewline && newlineBefore.has(current))
                this._builder.appendNewline();

            if (!this._builder.lastTokenWasWhitespace && addSpaceBefore.has(current)) {
                let shouldAddSpaceBefore = () => {
                    if (current === `(`) {
                        if (testCurrentLine(inAtSupportsRuleRegExp))
                            return false;
                        if (!testCurrentLine(inAtRuleRegExp))
                            return false;
                    }
                    return true;
                };
                if (shouldAddSpaceBefore())
                    this._builder.appendSpace();
            }

            while (this._builder.lastTokenWasWhitespace && removeSpaceBefore.has(current)) {
                let shouldRemoveSpaceBefore = () => {
                    if (current === `:`) {
                        if (!testCurrentLine(this._builder.currentLine.includes(`(`) ? inAtRuleRegExp : inAtRuleBeforeParenthesisRegExp)) {
                            if (!inProperty())
                                return false;
                        }
                    }
                    if (current === `(`) {
                        if (!testCurrentLine(lastTokenWasOpenParenthesisRegExp)) {
                            if (testCurrentLine(inAtRuleRegExp) && !testCurrentLine(inAtRuleAfterParenthesisRegExp))
                                return false;
                        }
                    }
                    return true;
                };
                if (!shouldRemoveSpaceBefore())
                    break;
                this._builder.removeLastWhitespace();
            }
        };

        let formatAfter = () => {
            while (this._builder.lastTokenWasWhitespace && removeSpaceAfter.has(current)) {
                let shouldRemoveSpaceAfter = () => {
                    if (current === `(`) {
                        if (!testCurrentLine(lastTokenWasOpenParenthesisRegExp)) {
                            if (!testCurrentLine(inAtRuleRegExp)) {
                                if (!inProperty())
                                    return false;
                            }
                        }
                    }
                    return true;
                };
                if (!shouldRemoveSpaceAfter())
                    break;
                this._builder.removeLastWhitespace();
            }

            if (!this._builder.lastTokenWasWhitespace && addSpaceAfter.has(current)) {
                let shouldAddSpaceAfter = () => {
                    if (current === `:`) {
                        if (!testCurrentLine(this._builder.currentLine.includes(`(`) ? inAtRuleRegExp : inAtRuleBeforeParenthesisRegExp)) {
                            if (!inProperty())
                                return false;
                        }
                    }
                    if (current === `)`) {
                        if (!testCurrentLine(inAtRuleRegExp)) {
                            if (!inProperty())
                                return false;
                        }
                    }
                    return true;
                };
                if (shouldAddSpaceAfter())
                    this._builder.appendSpace();
            }

            if (indentAfter.has(current))
                this._builder.indent();

            if (newlineAfter.has(current)) {
                if (current === `}`)
                    this._builder.appendMultipleNewlines(2);
                else
                    this._builder.appendNewline();
            }
        };

        for (; index < this._sourceText.length; ++index) {
            current = this._sourceText[index];

            let possibleSpecialSequence = null;
            if (quoteTypes.has(current))
                possibleSpecialSequence = {type: "quote", startIndex: index, endString: current};
            else if (current === `/` && this._sourceText[index + 1] === `*`)
                possibleSpecialSequence = {type: "comment", startIndex: index, endString: `*/`};
            else if (current === `u` && this._sourceText[index + 1] === `r` && this._sourceText[index + 2] === `l` && this._sourceText[index + 3] === `(`)
                possibleSpecialSequence = {type: "url", startIndex: index, endString: `)`};

            if (possibleSpecialSequence || specialSequenceStack.length) {
                let currentSpecialSequence = specialSequenceStack.lastValue;

                if (currentSpecialSequence?.type !== "comment") {
                    if (possibleSpecialSequence?.type !== "comment") {
                        let escapeCount = 0;
                        while (this._sourceText[index - 1 - escapeCount] === "\\")
                            ++escapeCount;
                        if (escapeCount % 2)
                            continue;
                    }

                    if (possibleSpecialSequence && (!currentSpecialSequence || currentSpecialSequence.type !== possibleSpecialSequence.type || currentSpecialSequence.endString !== possibleSpecialSequence.endString)) {
                        specialSequenceStack.push(possibleSpecialSequence);
                        continue;
                    }
                }

                if (Array.from(currentSpecialSequence.endString).some((item, i) => index + i < this._sourceText.length && this._sourceText[index - currentSpecialSequence.endString.length + i + 1] !== item))
                    continue;

                let inComment = specialSequenceStack.some((item) => item.type === "comment");

                specialSequenceStack.pop();
                if (specialSequenceStack.length)
                    continue;

                let specialSequenceText = this._sourceText.substring(currentSpecialSequence.startIndex, index + 1);

                let lastSourceNewlineWasMultiple = this._sourceText[currentSpecialSequence.startIndex - 1] === `\n` && this._sourceText[currentSpecialSequence.startIndex - 2] === `\n`;
                let lastAppendNewlineWasMultiple = this._builder.lastNewlineAppendWasMultiple;

                let commentOnOwnLine = false;
                if (inComment) {
                    commentOnOwnLine = testCurrentLine(whitespaceOnlyRegExp);

                    if (!commentOnOwnLine || lastAppendNewlineWasMultiple) {
                        while (this._builder.lastTokenWasNewline)
                            this._builder.removeLastNewline();
                    }

                    if (commentOnOwnLine) {
                        if (currentSpecialSequence.startIndex > 0 && !this._builder.indented)
                            this._builder.appendNewline();
                    } else if (this._builder.currentLine.length && !this._builder.lastTokenWasWhitespace)
                        this._builder.appendSpace();

                    if (this._builder.lastTokenWasNewline && lastSourceNewlineWasMultiple)
                        this._builder.appendNewline(true);
                }

                this._builder.appendStringWithPossibleNewlines(specialSequenceText, currentSpecialSequence.startIndex);

                if (inComment) {
                    if (commentOnOwnLine) {
                        if (lastAppendNewlineWasMultiple && !lastSourceNewlineWasMultiple)
                            this._builder.appendMultipleNewlines(2);
                        else
                            this._builder.appendNewline();
                    } else if (!/\s/.test(current)) {
                        if (!testCurrentLine(inAtRuleRegExp) && !inSelector() && !inProperty())
                            this._builder.appendNewline();
                        else
                            this._builder.appendSpace();
                    }
                }

                formatAfter();
                continue;
            }

            if (/\s/.test(current)) {
                if (current === `\n`) {
                    if (!this._builder.lastTokenWasNewline) {
                        while (this._builder.lastTokenWasWhitespace)
                            this._builder.removeLastWhitespace();
                        if (!removeSpaceAfter.has(this._builder.lastToken))
                            this._builder.appendNewline();
                        else
                            this._builder.appendSpace();
                    }
                } else if (!this._builder.lastTokenWasWhitespace && !removeSpaceAfter.has(this._builder.lastToken))
                    this._builder.appendSpace();
                continue;
            }

            if (current === `{`)
                ++depth;
            else if (current === `}`)
                --depth;

            formatBefore();
            this._builder.appendToken(current, index);
            formatAfter();
        }

        if (specialSequenceStack.length) {
            let firstSpecialSequence = specialSequenceStack[0];
            this._builder.appendStringWithPossibleNewlines(this._sourceText.substring(firstSpecialSequence.startIndex), firstSpecialSequence.startIndex);
        }

        this._builder.finish();
    }
};
