/*
** Copyright 2009-10 Martin Holmes, Meagan Timney and the
** University of Victoria Humanities Computing and Media
** Centre.

** This file is part of the projXMLEditor project which in
** turn belongs to the Image Markup Tool version 2.0
** project. The purpose of svgIconsTest is to provide a
** platform to test and learn various features of Qt, and
** to provide a semi-useful tool to aid in the rapid
** creation and editing of resource files containing SVG
** icons for Qt application development.

** GNU Lesser General Public License Usage
** This file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.

** You may also use this code under the Mozilla Public Licence
** version 1.1. MPL 1.1 can be found at http://www.mozilla.org/MPL/MPL-1.1.html.

** "svgIconsTest" 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 Lesser General Public License for more details.
**
** Modified 9-5-2011 by Aurelien DERAM
** colors changed to match Kate ones
*/

#include <QtGui>
#include <qregularexpression.h>

#include "Xmlhighlighter.h"

//! [0]
XmlHighlighter::XmlHighlighter(QTextDocument* parent)
    : QSyntaxHighlighter(parent) {
    HighlightingRule rule;

    //Taken from W3C: <http://www.w3.org/TR/REC-xml/#sec-suggested-names>
    //These are removed: \\x10000-\\xEFFFF (we can't fit them into the \xhhhh format).
    const QString nameStartCharList = R"(:A-Z_a-z\x00C0-\x00D6\x00D8-\x00F6\x00F8-\x02FF\x0370-\x037D\x037F-\x1FFF\x200C-\x200D\x2070-\x218F\x2C00-\x2FEF\x3001-\xD7FF\xF900-\xFDCF\xFDF0-\xFFFD)";

    const QString nameCharList = nameStartCharList + R"(\-\.0-9\x00B7\x0300-\x036F\x203F-\x2040)";
    const QString nameStart = "[" + nameStartCharList + "]";
    const QString nameChar = "[" + nameCharList + "]";
    const QString xmlName = nameStart + "(" + nameChar + ")*";

    QFont font;
    font.setFamily("DejaVu Sans Mono");
    font.setFixedPitch(true);
    font.setItalic(false);
    font.setPointSize(10);
    font.setStyle(QFont::StyleNormal);

    //Processing instructions.
    xmlProcInstStartExpression = QRegularExpression("<\\?");
    xmlProcInstEndExpression = QRegularExpression("\\?>");
    xmlProcInstFormat.setFont(font);
    xmlProcInstFormat.setForeground(Qt::black);
    xmlProcInstFormat.setFontItalic(false);

    //Multiline comments.
    xmlCommentStartExpression = QRegularExpression("<!\\-\\-");
    xmlCommentEndExpression = QRegularExpression("\\-\\->");
    xmlCommentFormat.setFont(font);
    xmlCommentFormat.setForeground(Qt::lightGray);
    xmlCommentFormat.setFontItalic(false);

    //Opening tags
    xmlOpenTagStartExpression = QRegularExpression("<" + xmlName);
    xmlOpenTagEndExpression = QRegularExpression(">");

    //Closing tags: first shot, handling them as start-end pairs.
    xmlCloseTagStartExpression = QRegularExpression("</" + xmlName);
    xmlCloseTagEndExpression = QRegularExpression(">");

    xmlTagFormat.setFont(font);
    xmlTagFormat.setForeground(Qt::black);
    xmlTagFormat.setFontWeight(QFont::Bold);
    xmlTagFormat.setFontItalic(false);

    //Attributes
    xmlAttributeStartExpression = QRegularExpression("\\s*" + xmlName + R"(\s*=\s*((\x0022)|(\x0027)))");
    xmlAttributeEndExpression = QRegularExpression(R"((([^\x0022]*\x0022)|([^\x0027]*\x0027)))");
    xmlAttributeFormat.setFont(font);
    xmlAttributeFormat.setForeground(Qt::darkGreen);
    xmlAttributeFormat.setFontItalic(false);

    //Attribute values.
    xmlAttValStartExpression = QRegularExpression(R"(((\x0022[^\x0022]*)|(\x0027[^\x0027]*)))");
    //The end expression varies depending on what's matched with the start expression (single or double quote).
    //This regexp is actually reset repeatedly during the highlighting process.
    xmlAttValFormat.setFont(font);
    xmlAttValFormat.setForeground(Qt::darkRed);
    xmlAttValFormat.setFontItalic(false);


    //The DOCTYPE declaration.
    xmlDoctypeStartExpression = QRegularExpression("<!DOCTYPE");
    xmlDoctypeEndExpression = QRegularExpression(">");
    xmlDoctypeFormat.setFont(font);
    xmlDoctypeFormat.setForeground(Qt::lightGray);
    xmlDoctypeFormat.setFontItalic(false);


    //Next, XML entities. This is the first item we can assume not to be multiline,
    //So this can be added to the rules. We add this to the second set of rules so
    //that it can highlight inside attribute values.
    xmlEntityFormat.setFont(font);
    xmlEntityFormat.setForeground(Qt::darkGray);
    xmlEntityFormat.setFontItalic(true);
    rule.pattern = QRegularExpression("&" + xmlName + ";");
    rule.format = xmlEntityFormat;
    hlRules.append(rule);

}

void XmlHighlighter::highlightSubBlock(const QString& text, const int startIndex, const int currState) {
    if (startIndex >= text.length()) {
        return;
    }
    int offset, commentOffset, procInstOffset, doctypeOffset, openTagOffset, closeTagOffset;
    int matchedLength = 0;
    int lowest = -1;
    int newState = -1;
    QRegularExpressionMatch match; // To store regular expression matches

    int effectiveState = currState;
    if (currState < 0) {
        effectiveState = inNothing;
    }
    switch (effectiveState) {
        case (inNothing): {
            //If we're not in anything, then what could be coming is either a comment, processing instruction, a doctype decl, or a tag (open or close).
            commentOffset = text.indexOf(xmlCommentStartExpression, startIndex, &match);
            if (commentOffset > lowest) {
                lowest = commentOffset;
                newState = inComment;
                matchedLength = match.capturedLength();
            }
            procInstOffset = text.indexOf(xmlProcInstStartExpression, startIndex, &match);
            if ((procInstOffset > -1) && ((lowest == -1) || (procInstOffset < lowest))) {
                lowest = procInstOffset;
                newState = inProcInst;
                matchedLength = match.capturedLength();
            }
            doctypeOffset = text.indexOf(xmlDoctypeStartExpression, startIndex, &match);
            if ((doctypeOffset > -1) && ((lowest == -1) || (doctypeOffset < lowest))) {
                lowest = doctypeOffset;
                newState = inDoctypeDecl;
                matchedLength = match.capturedLength();
            }
            openTagOffset = text.indexOf(xmlOpenTagStartExpression, startIndex, &match);
            if ((openTagOffset > -1) && ((lowest == -1) || (openTagOffset < lowest))) {
                lowest = openTagOffset;
                newState = inOpenTag;
                matchedLength = match.capturedLength();
            }
            closeTagOffset = text.indexOf(xmlCloseTagStartExpression, startIndex, &match);
            if ((closeTagOffset > -1) && ((lowest == -1) || (closeTagOffset < lowest))) {
                newState = inCloseTag;
                matchedLength = match.capturedLength();
            }
            switch (newState) {
                case -1: {
                    //Nothing starts in this block.
                    setCurrentBlockState(inNothing);
                    break;
                }
                case inComment: {
                    //We're into a comment.
                    setFormat(commentOffset, matchedLength, xmlCommentFormat);
                    setCurrentBlockState(inComment);
                    highlightSubBlock(text, commentOffset + matchedLength, inComment);
                    break;
                }
                case inProcInst: {
                    //We're into a processing instruction.
                    //Format the matched text
                    setFormat(procInstOffset, matchedLength, xmlProcInstFormat);
                    //Call this function again with a new offset and state.
                    setCurrentBlockState(inProcInst);
                    highlightSubBlock(text, procInstOffset + matchedLength, inProcInst);
                    break;
                }
                case inDoctypeDecl: {
                    //We're into a document type declaration.
                    //Format the matched text
                    setFormat(doctypeOffset, matchedLength, xmlDoctypeFormat);
                    //Call this function again with a new offset and state.
                    setCurrentBlockState(inDoctypeDecl);
                    highlightSubBlock(text, doctypeOffset + matchedLength, inDoctypeDecl);
                    break;
                }
                case inOpenTag: {
                    //We're into an opening tag.
                    //Format the matched text
                    setFormat(openTagOffset, matchedLength, xmlTagFormat);
                    //Call this function again with a new offset and state.
                    setCurrentBlockState(inOpenTag);
                    highlightSubBlock(text, openTagOffset + matchedLength, inOpenTag);
                    break;
                }
                case inCloseTag: {
                    //We're into a closing tag.
                    //Format the matched text
                    setFormat(closeTagOffset, matchedLength, xmlTagFormat);
                    setCurrentBlockState(inCloseTag);
                    //Call this function again with a new offset and state.
                    highlightSubBlock(text, closeTagOffset + matchedLength, inCloseTag);
                    break;
                }
            }
            break;
        }
        case inProcInst: {
            //Look for the end of the processing instruction.
            offset = text.indexOf(xmlProcInstEndExpression, startIndex, &match);
            matchedLength = match.capturedLength();
            if (offset > -1) {
                setFormat(startIndex, (offset + matchedLength) - startIndex, xmlProcInstFormat);
                setCurrentBlockState(inNothing);
                highlightSubBlock(text, offset + matchedLength, inNothing);
            }
            else {
                //We leave this block still inside the processing instruction,
                //after formatting the rest of the line.
                setFormat(startIndex, text.length() - startIndex, xmlProcInstFormat);
                setCurrentBlockState(inProcInst);
            }
            break;
        }
        case inDoctypeDecl: {
            //Look for the end of the doctype declaration.
            offset = text.indexOf(xmlDoctypeEndExpression, startIndex, &match);
            matchedLength = match.capturedLength();
            if (offset > -1) {
                setFormat(startIndex, (offset + matchedLength) - startIndex, xmlDoctypeFormat);
                setCurrentBlockState(inNothing);
                highlightSubBlock(text, offset + matchedLength, inNothing);
            }
            else {
                //We leave this block still inside the doctype declaration,
                //after formatting the rest of the line.
                setFormat(startIndex, text.length() - startIndex, xmlDoctypeFormat);
                setCurrentBlockState(inDoctypeDecl);
            }
            break;
        }
        case inOpenTag: {
//If we're in an open tag, we're looking either for the end of the open tag, or for
//the beginning of an attribute name.
            int attStartOffset = text.indexOf(xmlAttributeStartExpression, startIndex, &match);
            if (attStartOffset > -1) {
                lowest = attStartOffset;
                newState = inAttVal;
                matchedLength = match.capturedLength();
            }
            int openTagEndOffset = text.indexOf(xmlOpenTagEndExpression, startIndex, &match);
            if ((openTagEndOffset > -1) && ((lowest == -1) || (openTagEndOffset < lowest))) {
                newState = inNothing;
                matchedLength = match.capturedLength();
            }
            switch (newState) {
                case -1: {
                    //we're still in a tag. No need to highlight anything.
                    setCurrentBlockState(inOpenTag);
                    break;
                }
                case inNothing: {
                    //We've come to the end of the open tag.
                    setFormat(openTagEndOffset, matchedLength, xmlTagFormat);
                    setCurrentBlockState(inNothing);
                    highlightSubBlock(text, openTagEndOffset + matchedLength, inNothing);
                    break;
                }
                case inAttVal: {
                    //We've started an attribute. First format the attribute name and quote.
                    setFormat(attStartOffset, matchedLength, xmlAttributeFormat);
                    //We need to change the value of the attribute close matcher, so that
                    //it pairs correctly with the opening quote.
                    QChar quote = text.at(attStartOffset + matchedLength - 1);
                    if (quote.unicode() == 0x27) {
                        xmlAttributeEndExpression = QRegularExpression("\\x0027");
                    }
                    else {
                        xmlAttributeEndExpression = QRegularExpression("\\x0022");
                    }
                    setCurrentBlockState(inAttVal);
                    highlightSubBlock(text, attStartOffset + matchedLength, inAttVal);
                    break;
                }
            }
            break;
        }
        case inAttVal: {
//When we're in an attribute value, we're only looking for the closing quote.
            offset = text.indexOf(xmlAttributeEndExpression, startIndex);
            if (offset > -1) {
                //Do some highlighting. First the attribute value.
                setFormat(startIndex, offset - startIndex, xmlAttValFormat);
                //Now the closing quote.
                setFormat(offset, 1, xmlAttributeFormat);
                setCurrentBlockState(inOpenTag);
                highlightSubBlock(text, offset + 1, inOpenTag);
            }
            else {
                //The attribute value runs over the end of the line.
                setFormat(startIndex, text.length() - startIndex, xmlAttValFormat);
                setCurrentBlockState(inAttVal);
            }
            break;
        }
        case inCloseTag: {
            int closeTagEndOffset = text.indexOf(xmlCloseTagEndExpression, startIndex, &match);
            matchedLength = match.capturedLength();
            if (closeTagEndOffset > -1) {
                //We've found the end of the close tag.
                setFormat(closeTagEndOffset, matchedLength, xmlTagFormat);
                setCurrentBlockState(inNothing);
                highlightSubBlock(text, closeTagEndOffset + matchedLength, inNothing);
            }
            else {
                //There must be a linebreak inside the close tag.
                setCurrentBlockState(inCloseTag);
            }
            break;
        }
        case inComment: {
//Once we're in a comment, we just have to search for the end of it. Nothing else takes precedence.
            //Look for the end of the comment.
            offset = text.indexOf(xmlCommentEndExpression, startIndex, &match);
            matchedLength = match.capturedLength();
            if (offset > -1) {
                setFormat(startIndex, (offset + matchedLength) - startIndex, xmlCommentFormat);
                setCurrentBlockState(inNothing);
                highlightSubBlock(text, offset + matchedLength, inNothing);
            }
            else {
                //We leave this block still inside the comment,
                //after formatting the rest of the line.
                setFormat(startIndex, text.length() - startIndex, xmlCommentFormat);
                setCurrentBlockState(inComment);
            }
            break;
        }
    }

}

void XmlHighlighter::highlightBlock(const QString& text) {
    //Do the main block highlighting.
    highlightSubBlock(text, 0, previousBlockState());

    //Run the set of inline rules.
    for (const HighlightingRule& rule : hlRules) {
        QRegularExpression expression(rule.pattern);
        QRegularExpressionMatch match;
        match = expression.match(text);
        while (match.hasMatch()) {
            int length = match.capturedLength();
            setFormat(match.capturedStart(), length, rule.format);
            match = expression.match(text, match.capturedStart() + length);
        }
    }
}
