/************************************************************************
**
**  Copyright (C) 2009, 2010  Nokia Corporation, Strahinja Markovic
**
**  This file is part of Sigil.
**
**  Sigil 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.
**
**  Sigil 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 Sigil.  If not, see <http://www.gnu.org/licenses/>.
**
*************************************************************************/

#include "Misc/CSSHighlighter.h"
#include "Misc/SettingsStore.h"

// TODO: This code was written by Nokia for Qt Designer, and it's horribly ugly.
// I've tried to clean it up, but it's hopeless. Write a new CSS parser to replace this.

enum State {
    Selector,
    Property,
    Value,
    Pseudo,
    Pseudo1,
    Pseudo2,
    Quote,
    MaybeComment,
    Comment,
    MaybeCommentEnd
};

enum Token {
    ALNUM,
    LBRACE,
    RBRACE,
    COLON,
    SEMICOLON,
    COMMA,
    QUOTE,
    SLASH,
    STAR
};

// Transitions for the DFA
static const int transitions[ 10 ][ 9 ] = {
    { Selector, Property, Selector, Pseudo,  Property, Selector, Quote,   MaybeComment, Selector        }, // Selector
    { Property, Property, Selector, Value,   Property, Property, Quote,   MaybeComment, Property        }, // Property
    { Value,    Property, Selector, Value,   Property, Value,    Quote,   MaybeComment, Value           }, // Value
    { Pseudo1,  Property, Selector, Pseudo2, Selector, Selector, Quote,   MaybeComment, Pseudo          }, // Pseudo
    { Pseudo1,  Property, Selector, Pseudo,  Selector, Selector, Quote,   MaybeComment, Pseudo1         }, // Pseudo1
    { Pseudo2,  Property, Selector, Pseudo,  Selector, Selector, Quote,   MaybeComment, Pseudo2         }, // Pseudo2
    { Quote,    Quote,    Quote,    Quote,   Quote,    Quote,    -1,      Quote,        Quote           }, // Quote
    { -1,       -1,       -1,       -1,     -1,        -1,       -1,      -1,           Comment         }, // MaybeComment
    { Comment,  Comment,  Comment,  Comment, Comment,  Comment,  Comment, Comment,      MaybeCommentEnd }, // Comment
    { Comment,  Comment,  Comment,  Comment, Comment,  Comment,  Comment, -1,           MaybeCommentEnd }  // MaybeCommentEnd
};


CSSHighlighter::CSSHighlighter(QObject *parent)
    : QSyntaxHighlighter(parent)
{
    SettingsStore settings;
    m_codeViewAppearance = settings.codeViewAppearance();
}

void CSSHighlighter::highlightBlock(const QString &text)
{
    int lastIndex = 0;
    bool lastWasSlash = false;
    int state = previousBlockState();
    int save_state = 0;

    if (state == -1) {
        // As long as the text is empty, leave the state undetermined
        if (text.isEmpty()) {
            setCurrentBlockState(-1);
            return;
        }

        // The initial state is based on the presence of a ":" and the absence of a "{".
        // This is because Qt style sheets support both a full stylesheet as well as
        // an inline form with just properties.
        state = save_state = (text.indexOf(QLatin1Char(':')) > -1 &&
                              text.indexOf(QLatin1Char('{')) == -1) ? Property : Selector;
    } else {
        save_state = state >> 16;
        state &= 0x00ff;
    }

    if (state == MaybeCommentEnd) {
        state = Comment;
    } else if (state == MaybeComment) {
        state = save_state;
    }

    for (int i = 0; i < text.length(); i++) {
        int token = ALNUM;
        const char character = text.at(i).toLatin1();

        if (state == Quote) {
            if (character == '\\') {
                lastWasSlash = true;
            } else {
                if (character == '\"' && !lastWasSlash) {
                    token = QUOTE;
                }

                lastWasSlash = false;
            }
        } else {
            token = character == '{'  ? LBRACE    :
                    character == '}'  ? RBRACE    :
                    character == ':'  ? COLON     :
                    character == ';'  ? SEMICOLON :
                    character == ','  ? COMMA     :
                    character == '\"' ? QUOTE     :
                    character == '/'  ? SLASH     :
                    character == '*'  ? STAR      :
                    ALNUM;
        }

        int new_state = transitions[ state ][ token ];

        if (new_state != state) {
            bool include_token = new_state == MaybeCommentEnd ||
                                 (state == MaybeCommentEnd && new_state != Comment) ||
                                 state == Quote;
            highlight(text, lastIndex, i - lastIndex + include_token, state);

            if (new_state == Comment) {
                lastIndex = i - 1;    // include the slash and star
            } else {
                lastIndex = i + ((token == ALNUM || new_state == Quote) ? 0 : 1);
            }
        }

        if (new_state == -1) {
            state = save_state;
        } else if (state <= Pseudo2) {
            save_state = state;
            state = new_state;
        } else {
            state = new_state;
        }
    }

    highlight(text, lastIndex, text.length() - lastIndex, state);
    setCurrentBlockState(state + (save_state << 16));
}


void CSSHighlighter::highlight(const QString &text, int start, int length, int state)
{
    if (start >= text.length() || length <= 0) {
        return;
    }

    QTextCharFormat format;

    switch (state) {
        case Selector:
            setFormat(start, length, m_codeViewAppearance.css_selector_color);
            break;

        case Property:
            setFormat(start, length, m_codeViewAppearance.css_property_color);
            break;

        case Value:
            setFormat(start, length, m_codeViewAppearance.css_value_color);
            break;

        case Pseudo1:
            setFormat(start, length, m_codeViewAppearance.css_selector_color);
            break;

        case Pseudo2:
            setFormat(start, length, m_codeViewAppearance.css_selector_color);
            break;

        case Quote:
            setFormat(start, length, m_codeViewAppearance.css_quote_color);
            break;

        case Comment:
        case MaybeCommentEnd:
            format.setForeground(m_codeViewAppearance.css_comment_color);
            setFormat(start, length, format);
            break;

        default:
            break;
    }
}
