/*
 * Copyright (C) Pedram Pourang (aka Tsu Jan) 2014-2019 <tsujan2000@gmail.com>
 *
 * FeatherPad is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * FeatherPad is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @license GPL-3.0+ <https://spdx.org/licenses/GPL-3.0+.html>
 */

#include "highlighter.h"

namespace FeatherPad {

// This should be called before "htmlCSSHighlighter()" and "htmlJavascript()".
void Highlighter::htmlBrackets (const QString &text, const int start)
{
    if (progLan != "html") return;

    /*****************************
     * (Multiline) HTML Brackets *
     *****************************/

    int braIndex = start;
    int indx = 0;
    QRegularExpressionMatch startMatch;
    QRegularExpression braStartExp ("<(?!\\!)/{0,1}[A-Za-z0-9_\\-]+");
    QRegularExpressionMatch endMatch;
    QRegularExpression braEndExp (">");
    QRegularExpression styleExp ("<(style|STYLE)$|<(style|STYLE)\\s+[^>]*");
    bool isStyle (false);
    QTextCharFormat htmlBraFormat;
    htmlBraFormat.setFontWeight (QFont::Bold);
    htmlBraFormat.setForeground (Violet);

    int prevState = previousBlockState();
    if (braIndex > 0
        || (prevState != singleQuoteState && prevState != doubleQuoteState
            && (prevState < htmlBracketState || prevState > htmlStyleSingleQuoteState)))
    {
        braIndex = text.indexOf (braStartExp, start, &startMatch);
        while (format (braIndex) == commentFormat || format (braIndex) == urlFormat)
            braIndex = text.indexOf (braStartExp, braIndex + 1, &startMatch);
        if (braIndex > -1)
        {
            indx = text.indexOf (styleExp, start);
            while (format (indx) == commentFormat || format (indx) == urlFormat)
                indx = text.indexOf (styleExp, indx + 1);
            isStyle = indx > -1 && braIndex == indx;
        }
    }
    else if (braIndex == 0 && (prevState == htmlStyleState
                               /* these quote states are set only with styles */
                               || prevState == htmlStyleSingleQuoteState
                               || prevState == htmlStyleDoubleQuoteState))
    {
        isStyle = true;
    }

    int bn = currentBlock().blockNumber();
    bool mainFormatting (bn >= startCursor.blockNumber() && bn <= endCursor.blockNumber());
    //bool hugeText (text.length() > 50000);
    int firstBraIndex = braIndex; // to check progress in the following loop
    while (braIndex >= 0)
    {
        /*if (hugeText)
        {
            setFormat (braIndex, text.length() - braIndex, translucentFormat);
            break;
        }*/
        int braEndIndex;

        int matched = 0;
        if (braIndex == 0
            && (prevState == singleQuoteState || prevState == doubleQuoteState
                || (prevState >= htmlBracketState && prevState <= htmlStyleSingleQuoteState)))
        {
            braEndIndex = text.indexOf (braEndExp, 0, &endMatch);
        }
        else
        {
            matched = startMatch.capturedLength();
            braEndIndex = text.indexOf (braEndExp,
                                        braIndex + matched,
                                        &endMatch);
        }

        int len;
        if (braEndIndex == -1)
        {
            len = text.length() - braIndex;
            if (isStyle)
                setCurrentBlockState (htmlStyleState);
            else
                setCurrentBlockState (htmlBracketState);
        }
        else
        {
            len = braEndIndex - braIndex
                  + endMatch.capturedLength();
        }

        if (matched > 0)
            setFormat (braIndex, matched, htmlBraFormat);
        if (braEndIndex > -1)
            setFormat (braEndIndex, endMatch.capturedLength(), htmlBraFormat);


        int endLimit;
        if (braEndIndex == -1)
            endLimit = text.length();
        else
            endLimit = braEndIndex;

        /***************************
         * (Multiline) HTML Quotes *
         ***************************/

        int quoteIndex = braIndex;
        QRegularExpressionMatch quoteMatch;
        QRegularExpression quoteExpression ("\"|\'");
        int quote = doubleQuoteState;

        /* find the start quote */
        if (braIndex > firstBraIndex // when we're in another bracket
            || (prevState != doubleQuoteState
                && prevState != singleQuoteState
                && prevState != htmlStyleSingleQuoteState
                && prevState != htmlStyleDoubleQuoteState))
        {
            quoteIndex = text.indexOf (quoteExpression, braIndex);

            /* if the start quote is found... */
            if (quoteIndex >= braIndex && quoteIndex <= endLimit)
            {
                /* ... distinguish between double and single quotes */
                if (text.at (quoteIndex) == quoteMark.pattern().at (0))
                {
                    quoteExpression = quoteMark;
                    quote = currentBlockState() == htmlStyleState ? htmlStyleDoubleQuoteState
                                                                  : doubleQuoteState;
                }
                else
                {
                    quoteExpression.setPattern ("\'");
                    quote = currentBlockState() == htmlStyleState ? htmlStyleSingleQuoteState
                                                                  : singleQuoteState;
                }
            }
        }
        else // but if we're inside a quotation...
        {
            /* ... distinguish between the two quote kinds
               by checking the previous line */
            quote = prevState;
            if (quote == doubleQuoteState || quote == htmlStyleDoubleQuoteState)
                quoteExpression = quoteMark;
            else
                quoteExpression.setPattern ("\'");
        }

        while (quoteIndex >= braIndex && quoteIndex <= endLimit)
        {
            /* if the search is continued... */
            if (quoteExpression.pattern() == "\"|\'")
            {
                /* ... distinguish between double and single quotes
                   again because the quote mark may have changed */
                if (text.at (quoteIndex) == quoteMark.pattern().at (0))
                {
                    quoteExpression = quoteMark;
                    quote = currentBlockState() == htmlStyleState ? htmlStyleDoubleQuoteState
                                                                  : doubleQuoteState;
                }
                else
                {
                    quoteExpression.setPattern ("\'");
                    quote = currentBlockState() == htmlStyleState ? htmlStyleSingleQuoteState
                                                                  : singleQuoteState;
                }
            }

            int quoteEndIndex = text.indexOf (quoteExpression, quoteIndex + 1, &quoteMatch);
            if (quoteIndex == braIndex
                && (prevState == doubleQuoteState
                    || prevState == singleQuoteState
                    || prevState == htmlStyleSingleQuoteState
                    || prevState == htmlStyleDoubleQuoteState))
            {
                quoteEndIndex = text.indexOf (quoteExpression, braIndex, &quoteMatch);
            }

            int Matched = 0;
            if (quoteEndIndex == -1)
            {
                if (braEndIndex > -1) quoteEndIndex = braEndIndex;
            }
            else
            {
                if (quoteEndIndex > endLimit)
                    quoteEndIndex = endLimit;
                else
                    Matched = quoteMatch.capturedLength();
            }

            int quoteLength;
            if (quoteEndIndex == -1)
            {
                if (currentBlockState() == htmlBracketState
                    || currentBlockState() == htmlStyleState)
                {
                    setCurrentBlockState (quote);
                }
                quoteLength = text.length() - quoteIndex;
            }
            else
                quoteLength = quoteEndIndex - quoteIndex
                              + Matched;
            setFormat (quoteIndex, quoteLength, quoteExpression == quoteMark ? quoteFormat
                                                                             : altQuoteFormat);

            /* the next quote may be different */
            quoteExpression.setPattern ("\"|\'");
            quoteIndex = text.indexOf (quoteExpression, quoteIndex + quoteLength);
        }

        /*******************************
         * (Multiline) HTML Attributes *
         *******************************/

        if (mainFormatting)
        {
            QTextCharFormat htmlAttributeFormat;
            htmlAttributeFormat.setFontItalic (true);
            htmlAttributeFormat.setForeground (Brown);
            QRegularExpressionMatch attMatch;
            QRegularExpression attExp ("[A-Za-z0-9_\\-]+(?=\\s*\\=)");
            int attIndex = text.indexOf (attExp, braIndex, &attMatch);
            QTextCharFormat fi = format (attIndex);
            while (fi == quoteFormat || fi == altQuoteFormat || fi == urlInsideQuoteFormat)
            {
                attIndex += attMatch.capturedLength();
                fi = format (attIndex);
                while (fi == quoteFormat || fi == altQuoteFormat || fi == urlInsideQuoteFormat)
                {
                    ++ attIndex;
                    fi = format (attIndex);
                }
                attIndex = text.indexOf (attExp, attIndex, &attMatch);
                fi = format (attIndex);
            }
            while (attIndex >= braIndex && attIndex < endLimit)
            {
                setFormat (attIndex, attMatch.capturedLength(), htmlAttributeFormat);
                attIndex = text.indexOf(attExp, attIndex + attMatch.capturedLength(), &attMatch);
                fi = format (attIndex);
                while (fi == quoteFormat || fi == altQuoteFormat || fi == urlInsideQuoteFormat)
                {
                    attIndex += attMatch.capturedLength();
                    fi = format (attIndex);
                    while (fi == quoteFormat || fi == altQuoteFormat || fi == urlInsideQuoteFormat)
                    {
                        ++ attIndex;
                        fi = format (attIndex);
                    }
                    attIndex = text.indexOf (attExp, attIndex, &attMatch);
                    fi = format (attIndex);
                }
            }
        }

        indx = braIndex + len;
        braIndex = text.indexOf (braStartExp, braIndex + len, &startMatch);
        while (format (braIndex) == commentFormat || format (braIndex) == urlFormat)
            braIndex = text.indexOf (braStartExp, braIndex + 1, &startMatch);
        if (braIndex > -1)
        {
            indx = text.indexOf (styleExp, indx);
            while (format (indx) == commentFormat || format (indx) == urlFormat)
                indx = text.indexOf (styleExp, indx + 1);
            isStyle = indx > -1 && braIndex == indx;
        }
        else isStyle = false;
    }

    /* at last, format whitespaces */
    if (mainFormatting)
    {
        static_cast<TextBlockData *>(currentBlock().userData())->setHighlighted(); // completely highlighted
        for (const HighlightingRule &rule : qAsConst (highlightingRules))
        {
            if (rule.format == whiteSpaceFormat)
            {
                QRegularExpressionMatch match;
                int index = text.indexOf (rule.pattern, start, &match);
                while (index >= 0)
                {
                    setFormat (index, match.capturedLength(), rule.format);
                    index = text.indexOf (rule.pattern, index + match.capturedLength(), &match);
                }

                /* also, mark encoded and unencoded ampersands */
                QRegularExpression ampersand ("&");
                QTextCharFormat errorFormat;
                errorFormat.setFontUnderline (true);
                errorFormat.setForeground (Red);
                QTextCharFormat encodedFormat;
                encodedFormat.setForeground (DarkMagenta);
                encodedFormat.setFontItalic (true);

                index = text.indexOf (ampersand, start);
                while (index >= 0 && format (index) != mainFormat)
                    index = text.indexOf (ampersand, index + 1);
                while (index >= 0)
                {
                    QString str = text.mid (index, 6);
                    if (str == "&nbsp;")
                        setFormat (index, 6, encodedFormat);
                    else if (str.startsWith("&amp;"))
                        setFormat (index, 5, encodedFormat);
                    else if (str.startsWith ("&lt;") || str.startsWith ("&gt;"))
                        setFormat (index, 4, encodedFormat);
                    else
                        setFormat (index, 1, errorFormat);
                    index = text.indexOf (ampersand, index + 1);
                    while (index >= 0 && format (index) != mainFormat)
                        index = text.indexOf (ampersand, index + 1);
                }

                break;
            }
        }
    }
}
/*************************/
void Highlighter::htmlCSSHighlighter (const QString &text, const int start)
{
    if (progLan != "html") return;

    int cssIndex = start;

    QRegularExpressionMatch startMatch;
    QRegularExpression cssStartExp ("<(style|STYLE)>|<(style|STYLE)\\s+[^>]*>");
    QRegularExpressionMatch endMatch;
    QRegularExpression cssEndExp ("</(style|STYLE)\\s*>");
    QRegularExpressionMatch braMatch;
    QRegularExpression braEndExp (">");

    /* switch to css temporarily */
    commentStartExpression.setPattern ("/\\*");
    commentEndExpression.setPattern ("\\*/");
    progLan = "css";

    bool wasCSS (false);
    int prevState = previousBlockState();
    bool wasStyle (prevState == htmlStyleState
                   || prevState == htmlStyleSingleQuoteState
                   || prevState == htmlStyleDoubleQuoteState);
    QTextBlock prevBlock = currentBlock().previous();
    if (prevBlock.isValid())
    {
        if (TextBlockData *prevData = static_cast<TextBlockData *>(prevBlock.userData()))
            wasCSS = prevData->labelInfo() == "CSS"; // it's labeled below
    }

    QTextCharFormat fi;
    int matched = 0;
    if ((!wasCSS || start > 0)  && !wasStyle)
    {
        cssIndex = text.indexOf (cssStartExp, start, &startMatch);
        fi = format (cssIndex);
        while (cssIndex >= 0
               && (fi == commentFormat
                   || fi == quoteFormat || fi == altQuoteFormat || fi == urlInsideQuoteFormat))
        {
            cssIndex = text.indexOf (cssStartExp, cssIndex + startMatch.capturedLength(), &startMatch);
            fi = format (cssIndex);
        }
    }
    else if (wasStyle)
    {
        cssIndex = text.indexOf (braEndExp, start, &braMatch);
        fi = format (cssIndex);
        while (cssIndex >= 0
               && (fi == commentFormat || fi == urlFormat
                   || fi == quoteFormat || fi == altQuoteFormat || fi == urlInsideQuoteFormat))
        {
            cssIndex = text.indexOf (braEndExp, cssIndex + 1, &braMatch);
            fi = format (cssIndex);
        }
        if (cssIndex > -1)
            matched = braMatch.capturedLength(); // 1
    }
    TextBlockData *curData = static_cast<TextBlockData *>(currentBlock().userData());
    int bn = currentBlock().blockNumber();
    bool mainFormatting (bn >= startCursor.blockNumber() && bn <= endCursor.blockNumber());
    while (cssIndex >= 0)
    {
        /* single-line style bracket (<style ...>) */
        if (matched == 0 && (!wasCSS || cssIndex > 0))
            matched = startMatch.capturedLength();

        /* starting from here, clear all html formats...
           (NOTE: "mainFormat" is used instead of "neutralFormat"
           for HTML ampersands to be formatted correctly.) */
        setFormat (cssIndex + matched, text.length() - cssIndex - matched, mainFormat);
        setCurrentBlockState (0);

        /* ... and apply the css formats */
        multiLineQuote (text, cssIndex + matched, htmlCSSCommentState);
        int cssIndx = cssHighlighter (text, mainFormatting, cssIndex + matched);
        multiLineComment (text,
                          cssIndex + matched, cssIndx,
                          commentStartExpression, commentEndExpression,
                          htmlCSSCommentState,
                          commentFormat);
        if (mainFormatting)
        {
            for (const HighlightingRule &rule : qAsConst (highlightingRules))
            { // CSS doesn't have any main formatting except for witesapces
                if (rule.format == whiteSpaceFormat)
                {
                    QRegularExpressionMatch match;
                    int index = text.indexOf (rule.pattern, start, &match);
                    while (index >= 0)
                    {
                        setFormat (index, match.capturedLength(), rule.format);
                        index = text.indexOf (rule.pattern, index + match.capturedLength(), &match);
                    }
                }
            }
        }

        /* now, search for the end of the css block */
        int cssEndIndex;
        if (cssIndex == 0 && wasCSS)
            cssEndIndex = text.indexOf (cssEndExp, 0, &endMatch);
        else
        {
            cssEndIndex = text.indexOf (cssEndExp,
                                        cssIndex + matched,
                                        &endMatch);
        }

        fi = format (cssEndIndex);
        while (cssEndIndex > -1
               && (fi == quoteFormat || fi == altQuoteFormat || fi == urlInsideQuoteFormat
                   || fi == commentFormat || fi == urlFormat))
        {
            cssEndIndex = text.indexOf (cssEndExp, cssEndIndex + endMatch.capturedLength(), &endMatch);
            fi = format (cssEndIndex);
        }

        int len;
        if (cssEndIndex == -1)
        {
            if (currentBlockState() == 0)
                setCurrentBlockState (htmlCSSState); // for updating the next line
            /* Since the next line couldn't be processed based on the state of this line,
               we label this line to show that it's written in css and not html. */
            curData->insertInfo ("CSS");
            len = text.length() - cssIndex;
        }
        else
        {
            len = cssEndIndex - cssIndex
                  + endMatch.capturedLength();
            /* if the css block ends at this line, format
               the rest of the line as an html code again */
            setFormat (cssEndIndex, text.length() - cssEndIndex, mainFormat);
            setCurrentBlockState (0);
            progLan = "html";
            htmlBrackets (text, cssEndIndex);
            progLan = "css";
        }

        cssIndex = text.indexOf (cssStartExp, cssIndex + len, &startMatch);
        fi = format (cssIndex);
        while (cssIndex >= 0
               && (fi == commentFormat || fi == urlFormat
                   || fi == quoteFormat || fi == altQuoteFormat || fi == urlInsideQuoteFormat))
        {
            cssIndex = text.indexOf (cssStartExp, cssIndex + startMatch.capturedLength(), &startMatch);
            fi = format (cssIndex);
        }
        matched = 0; // single-line style bracket (<style ...>)
    }

    /* revert to html */
    progLan = "html";
    commentStartExpression.setPattern ("<!--");
    commentEndExpression.setPattern ("-->");
}
/*************************/
void Highlighter::htmlJavascript (const QString &text)
{
    if (progLan != "html") return;

    int javaIndex = 0;

    QRegularExpressionMatch startMatch;
    //QRegularExpression javaStartExp ("<(script|SCRIPT)\\s+[^<>]*(((language|LANGUAGE)\\s*\\=\\s*\"\\s*)|((TYPE|type)\\s*\\=\\s*\"\\s*(TEXT|text)/))(JavaScript|javascript)\\s*\"[^<>]*>");
    QRegularExpression javaStartExp ("<(script|SCRIPT)\\s*>|<(script|SCRIPT)\\s+[^<>]*>");
    QRegularExpressionMatch endMatch;
    QRegularExpression javaEndExp ("</(script|SCRIPT)\\s*>");

    /* switch to javascript temporarily */
    commentStartExpression.setPattern ("/\\*");
    commentEndExpression.setPattern ("\\*/");
    progLan = "javascript";

    bool wasJavascript (false);
    QTextBlock prevBlock = currentBlock().previous();
    if (prevBlock.isValid())
    {
        if (TextBlockData *prevData = static_cast<TextBlockData *>(prevBlock.userData()))
            wasJavascript = prevData->labelInfo() == "JS"; // it's labeled below
    }

    QTextCharFormat fi;
    if (!wasJavascript)
    {
        javaIndex = text.indexOf (javaStartExp, 0, &startMatch);
        fi = format (javaIndex);
        while (javaIndex >= 0
               && (fi == commentFormat || fi == urlFormat
                   || fi == quoteFormat || fi == altQuoteFormat || fi == urlInsideQuoteFormat))
        {
            javaIndex = text.indexOf (javaStartExp, javaIndex + startMatch.capturedLength(), &startMatch);
            fi = format (javaIndex);
        }
    }
    int matched = 0;
    TextBlockData *curData = static_cast<TextBlockData *>(currentBlock().userData());
    int bn = currentBlock().blockNumber();
    bool mainFormatting (bn >= startCursor.blockNumber() && bn <= endCursor.blockNumber());
    while (javaIndex >= 0)
    {
        if (!wasJavascript || javaIndex > 0)
        {
            matched = startMatch.capturedLength();
            curData->insertLastFormattedQuote (javaIndex + matched);
            curData->insertLastFormattedRegex (javaIndex + matched);
        }

        /* starting from here, clear all html formats... */
        setFormat (javaIndex + matched, text.length() - javaIndex - matched, mainFormat);
        setCurrentBlockState (0);

        /* ... and apply the javascript formats */
        singleLineComment (text, javaIndex + matched);
        multiLineQuote (text, javaIndex + matched, htmlJavaCommentState);
        multiLineComment (text,
                          javaIndex + matched, -1,
                          commentStartExpression, commentEndExpression,
                          htmlJavaCommentState,
                          commentFormat);
        multiLineRegex (text, javaIndex + matched);
        if (mainFormatting)
        {
            for (const HighlightingRule &rule : qAsConst (highlightingRules))
            {
                if (rule.format == commentFormat)
                    continue;

                QRegularExpressionMatch match;
                int index = text.indexOf (rule.pattern, javaIndex + matched, &match);
                if (rule.format != whiteSpaceFormat)
                {
                    fi = format (index);
                    while (index >= 0
                           && (fi == quoteFormat || fi == altQuoteFormat || fi == urlInsideQuoteFormat
                               || fi == commentFormat || fi == urlFormat
                               || fi ==  regexFormat))
                    {
                        index = text.indexOf (rule.pattern, index + match.capturedLength(), &match);
                        fi = format (index);
                    }
                }

                while (index >= 0)
                {
                    setFormat (index, match.capturedLength(), rule.format);
                    index = text.indexOf (rule.pattern, index + match.capturedLength(), &match);

                    if (rule.format != whiteSpaceFormat)
                    {
                        fi = format (index);
                        while (index >= 0
                               && (fi == quoteFormat || fi == altQuoteFormat || fi == urlInsideQuoteFormat
                                   || fi == commentFormat || fi == urlFormat
                                   || fi == regexFormat))
                        {
                            index = text.indexOf (rule.pattern, index + match.capturedLength(), &match);
                            fi = format (index);
                        }
                    }
                }
            }
        }

        /* now, search for the end of the javascript block */
        int javaEndIndex;
        if (javaIndex == 0 && wasJavascript)
            javaEndIndex = text.indexOf (javaEndExp, 0, &endMatch);
        else
        {
            javaEndIndex = text.indexOf (javaEndExp,
                                         javaIndex + matched,
                                         &endMatch);
        }

        fi = format (javaEndIndex);
        while (javaEndIndex > -1
               && (fi == quoteFormat || fi == altQuoteFormat || fi == urlInsideQuoteFormat
                   || fi == commentFormat || fi == urlFormat
                   || fi == regexFormat))
        {
            javaEndIndex = text.indexOf (javaEndExp, javaEndIndex + endMatch.capturedLength(), &endMatch);
            fi = format (javaEndIndex);
        }

        int len;
        if (javaEndIndex == -1)
        {
            if (currentBlockState() == 0)
                setCurrentBlockState (htmlJavaState); // for updating the next line
            /* Since the next line couldn't be processed based on the state of this line,
               we label this line to show that it's written in javascript and not html. */
            curData->insertInfo ("JS");
            len = text.length() - javaIndex;
        }
        else
        {
            len = javaEndIndex - javaIndex
                  + endMatch.capturedLength();
            /* if the javascript block ends at this line,
               format the rest of the line as an html code again */
            setFormat (javaEndIndex, text.length() - javaEndIndex, mainFormat);
            setCurrentBlockState (0);
            progLan = "html";
            htmlBrackets (text, javaEndIndex);
            htmlCSSHighlighter (text, javaEndIndex);
            progLan = "javascript";
        }

        javaIndex = text.indexOf (javaStartExp, javaIndex + len, &startMatch);
        fi = format (javaEndIndex);
        while (javaIndex > -1
               && (fi == commentFormat || fi == urlFormat
                   || fi == quoteFormat || fi == altQuoteFormat || fi == urlInsideQuoteFormat))
        {
            javaIndex = text.indexOf (javaStartExp, javaIndex + startMatch.capturedLength(), &startMatch);
            fi = format (javaEndIndex);
        }
    }

    /* revert to html */
    progLan = "html";
    commentStartExpression.setPattern ("<!--");
    commentEndExpression.setPattern ("-->");
}

}
