// Scintilla source code edit control
/** @file LexCmake.cxx
 ** Lexer for Cmake
 **/
// Copyright 2007 by Cristian Adam <cristian [dot] adam [at] gmx [dot] net>
// based on the NSIS lexer
// The License.txt file describes the conditions under which this software may be distributed.

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <assert.h>
#include <ctype.h>

#include <string>
#include <string_view>

#include "ILexer.h"
#include "Scintilla.h"
#include "SciLexer.h"

#include "WordList.h"
#include "LexAccessor.h"
#include "Accessor.h"
#include "StyleContext.h"
#include "CharacterSet.h"
#include "LexerModule.h"

using namespace Lexilla;

static bool isCmakeNumber(char ch)
{
    return(ch >= '0' && ch <= '9');
}

static bool isCmakeChar(char ch)
{
    return(ch == '.' ) || (ch == '_' ) || isCmakeNumber(ch) || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
}

static bool isCmakeLetter(char ch)
{
    return(ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
}

static bool CmakeNextLineHasElse(Sci_PositionU start, Sci_PositionU end, Accessor &styler)
{
    Sci_Position nNextLine = -1;
    for ( Sci_PositionU i = start; i < end; i++ ) {
        char cNext = styler.SafeGetCharAt( i );
        if ( cNext == '\n' ) {
            nNextLine = i+1;
            break;
        }
    }

    if ( nNextLine == -1 ) // We never foudn the next line...
        return false;

    for ( Sci_PositionU firstChar = nNextLine; firstChar < end; firstChar++ ) {
        char cNext = styler.SafeGetCharAt( firstChar );
        if ( cNext == ' ' )
            continue;
        if ( cNext == '\t' )
            continue;
        if ( styler.Match(firstChar, "ELSE")  || styler.Match(firstChar, "else"))
            return true;
        break;
    }

    return false;
}

static int calculateFoldCmake(Sci_PositionU start, Sci_PositionU end, int foldlevel, Accessor &styler, bool bElse)
{
    // If the word is too long, it is not what we are looking for
    if ( end - start > 20 )
        return foldlevel;

    int newFoldlevel = foldlevel;

    char s[20]; // The key word we are looking for has atmost 13 characters
    for (unsigned int i = 0; i < end - start + 1 && i < 19; i++) {
        s[i] = static_cast<char>( styler[ start + i ] );
        s[i + 1] = '\0';
    }

    if ( CompareCaseInsensitive(s, "IF") == 0 || CompareCaseInsensitive(s, "WHILE") == 0
         || CompareCaseInsensitive(s, "MACRO") == 0 || CompareCaseInsensitive(s, "FOREACH") == 0
         || CompareCaseInsensitive(s, "FUNCTION") == 0)
        newFoldlevel++;
    else if ( CompareCaseInsensitive(s, "ENDIF") == 0 || CompareCaseInsensitive(s, "ENDWHILE") == 0
              || CompareCaseInsensitive(s, "ENDMACRO") == 0 || CompareCaseInsensitive(s, "ENDFOREACH") == 0
              || CompareCaseInsensitive(s, "ENDFUNCTION") == 0)
        newFoldlevel--;
    else if ( bElse && CompareCaseInsensitive(s, "ELSEIF") == 0 )
        newFoldlevel++;
    else if ( bElse && CompareCaseInsensitive(s, "ELSE") == 0 )
        newFoldlevel++;

    return newFoldlevel;
}

static int classifyWordCmake(Sci_PositionU start, Sci_PositionU end, WordList *keywordLists[], Accessor &styler )
{
    char word[100] = {0};
    char lowercaseWord[100] = {0};

    WordList &Commands = *keywordLists[0];
    WordList &Parameters = *keywordLists[1];
    WordList &UserDefined = *keywordLists[2];

    for (Sci_PositionU i = 0; i < end - start + 1 && i < 99; i++) {
        word[i] = static_cast<char>( styler[ start + i ] );
        lowercaseWord[i] = static_cast<char>(tolower(word[i]));
    }

    // Check for special words...
    if ( CompareCaseInsensitive(word, "MACRO") == 0 || CompareCaseInsensitive(word, "ENDMACRO") == 0 )
        return SCE_CMAKE_MACRODEF;

    if ( CompareCaseInsensitive(word, "IF") == 0 ||  CompareCaseInsensitive(word, "ENDIF") == 0 )
        return SCE_CMAKE_IFDEFINEDEF;

    if ( CompareCaseInsensitive(word, "ELSEIF") == 0  || CompareCaseInsensitive(word, "ELSE") == 0 )
        return SCE_CMAKE_IFDEFINEDEF;

    if ( CompareCaseInsensitive(word, "WHILE") == 0 || CompareCaseInsensitive(word, "ENDWHILE") == 0)
        return SCE_CMAKE_WHILEDEF;

    if ( CompareCaseInsensitive(word, "FOREACH") == 0 || CompareCaseInsensitive(word, "ENDFOREACH") == 0)
        return SCE_CMAKE_FOREACHDEF;

    if ( Commands.InList(lowercaseWord) )
        return SCE_CMAKE_COMMANDS;

    if ( Parameters.InList(word) )
        return SCE_CMAKE_PARAMETERS;


    if ( UserDefined.InList(word) )
        return SCE_CMAKE_USERDEFINED;

    if ( strlen(word) > 3 ) {
        if ( word[1] == '{' && word[strlen(word)-1] == '}' )
            return SCE_CMAKE_VARIABLE;
    }

    // To check for numbers
    if ( isCmakeNumber( word[0] ) ) {
        bool bHasSimpleCmakeNumber = true;
        for (unsigned int j = 1; j < end - start + 1 && j < 99; j++) {
            if ( !isCmakeNumber( word[j] ) ) {
                bHasSimpleCmakeNumber = false;
                break;
            }
        }

        if ( bHasSimpleCmakeNumber )
            return SCE_CMAKE_NUMBER;
    }

    return SCE_CMAKE_DEFAULT;
}

static void ColouriseCmakeDoc(Sci_PositionU startPos, Sci_Position length, int, WordList *keywordLists[], Accessor &styler)
{
    int state = SCE_CMAKE_DEFAULT;
    if ( startPos > 0 )
        state = styler.StyleAt(startPos-1); // Use the style from the previous line, usually default, but could be commentbox

    styler.StartAt( startPos );
    styler.GetLine( startPos );

    Sci_PositionU nLengthDoc = startPos + length;
    styler.StartSegment( startPos );

    char cCurrChar;
    bool bVarInString = false;
    bool bClassicVarInString = false;

    Sci_PositionU i;
    for ( i = startPos; i < nLengthDoc; i++ ) {
        cCurrChar = styler.SafeGetCharAt( i );
        char cNextChar = styler.SafeGetCharAt(i+1);

        switch (state) {
        case SCE_CMAKE_DEFAULT:
            if ( cCurrChar == '#' ) { // we have a comment line
                styler.ColourTo(i-1, state );
                state = SCE_CMAKE_COMMENT;
                break;
            }
            if ( cCurrChar == '"' ) {
                styler.ColourTo(i-1, state );
                state = SCE_CMAKE_STRINGDQ;
                bVarInString = false;
                bClassicVarInString = false;
                break;
            }
            if ( cCurrChar == '\'' ) {
                styler.ColourTo(i-1, state );
                state = SCE_CMAKE_STRINGRQ;
                bVarInString = false;
                bClassicVarInString = false;
                break;
            }
            if ( cCurrChar == '`' ) {
                styler.ColourTo(i-1, state );
                state = SCE_CMAKE_STRINGLQ;
                bVarInString = false;
                bClassicVarInString = false;
                break;
            }

            // CMake Variable
            if ( cCurrChar == '$' || isCmakeChar(cCurrChar)) {
                styler.ColourTo(i-1,state);
                state = SCE_CMAKE_VARIABLE;

                // If it is a number, we must check and set style here first...
                if ( isCmakeNumber(cCurrChar) && (cNextChar == '\t' || cNextChar == ' ' || cNextChar == '\r' || cNextChar == '\n' ) )
                    styler.ColourTo( i, SCE_CMAKE_NUMBER);

                break;
            }

            break;
        case SCE_CMAKE_COMMENT:
            if ( cCurrChar == '\n' || cCurrChar == '\r' ) {
                if ( styler.SafeGetCharAt(i-1) == '\\' ) {
                    styler.ColourTo(i-2,state);
                    styler.ColourTo(i-1,SCE_CMAKE_DEFAULT);
                }
                else {
                    styler.ColourTo(i-1,state);
                    state = SCE_CMAKE_DEFAULT;
                }
            }
            break;
        case SCE_CMAKE_STRINGDQ:
        case SCE_CMAKE_STRINGLQ:
        case SCE_CMAKE_STRINGRQ:

            if ( styler.SafeGetCharAt(i-1) == '\\' && styler.SafeGetCharAt(i-2) == '$' )
                break; // Ignore the next character, even if it is a quote of some sort

            if ( cCurrChar == '"' && state == SCE_CMAKE_STRINGDQ ) {
                styler.ColourTo(i,state);
                state = SCE_CMAKE_DEFAULT;
                break;
            }

            if ( cCurrChar == '`' && state == SCE_CMAKE_STRINGLQ ) {
                styler.ColourTo(i,state);
                state = SCE_CMAKE_DEFAULT;
                break;
            }

            if ( cCurrChar == '\'' && state == SCE_CMAKE_STRINGRQ ) {
                styler.ColourTo(i,state);
                state = SCE_CMAKE_DEFAULT;
                break;
            }

            if ( cNextChar == '\r' || cNextChar == '\n' ) {
                Sci_Position nCurLine = styler.GetLine(i+1);
                Sci_Position nBack = i;
                // We need to check if the previous line has a \ in it...
                bool bNextLine = false;

                while ( nBack > 0 ) {
                    if ( styler.GetLine(nBack) != nCurLine )
                        break;

                    char cTemp = styler.SafeGetCharAt(nBack, 'a'); // Letter 'a' is safe here

                    if ( cTemp == '\\' ) {
                        bNextLine = true;
                        break;
                    }
                    if ( cTemp != '\r' && cTemp != '\n' && cTemp != '\t' && cTemp != ' ' )
                        break;

                    nBack--;
                }

                if ( bNextLine ) {
                    styler.ColourTo(i+1,state);
                }
                if ( bNextLine == false ) {
                    styler.ColourTo(i,state);
                    state = SCE_CMAKE_DEFAULT;
                }
            }
            break;

        case SCE_CMAKE_VARIABLE:

            // CMake Variable:
            if ( cCurrChar == '$' )
                state = SCE_CMAKE_DEFAULT;
            else if ( cCurrChar == '\\' && (cNextChar == 'n' || cNextChar == 'r' || cNextChar == 't' ) )
                state = SCE_CMAKE_DEFAULT;
            else if ( (isCmakeChar(cCurrChar) && !isCmakeChar( cNextChar) && cNextChar != '}') || cCurrChar == '}' ) {
                state = classifyWordCmake( styler.GetStartSegment(), i, keywordLists, styler );
                styler.ColourTo( i, state);
                state = SCE_CMAKE_DEFAULT;
            }
            else if ( !isCmakeChar( cCurrChar ) && cCurrChar != '{' && cCurrChar != '}' ) {
                if ( classifyWordCmake( styler.GetStartSegment(), i-1, keywordLists, styler) == SCE_CMAKE_NUMBER )
                    styler.ColourTo( i-1, SCE_CMAKE_NUMBER );

                state = SCE_CMAKE_DEFAULT;

                if ( cCurrChar == '"' ) {
                    state = SCE_CMAKE_STRINGDQ;
                    bVarInString = false;
                    bClassicVarInString = false;
                }
                else if ( cCurrChar == '`' ) {
                    state = SCE_CMAKE_STRINGLQ;
                    bVarInString = false;
                    bClassicVarInString = false;
                }
                else if ( cCurrChar == '\'' ) {
                    state = SCE_CMAKE_STRINGRQ;
                    bVarInString = false;
                    bClassicVarInString = false;
                }
                else if ( cCurrChar == '#' ) {
                    state = SCE_CMAKE_COMMENT;
                }
            }
            break;
        }

        if ( state == SCE_CMAKE_STRINGDQ || state == SCE_CMAKE_STRINGLQ || state == SCE_CMAKE_STRINGRQ ) {
            bool bIngoreNextDollarSign = false;

            if ( bVarInString && cCurrChar == '$' ) {
                bVarInString = false;
                bIngoreNextDollarSign = true;
            }
            else if ( bVarInString && cCurrChar == '\\' && (cNextChar == 'n' || cNextChar == 'r' || cNextChar == 't' || cNextChar == '"' || cNextChar == '`' || cNextChar == '\'' ) ) {
                styler.ColourTo( i+1, SCE_CMAKE_STRINGVAR);
                bVarInString = false;
                bIngoreNextDollarSign = false;
            }

            else if ( bVarInString && !isCmakeChar(cNextChar) ) {
                int nWordState = classifyWordCmake( styler.GetStartSegment(), i, keywordLists, styler);
                if ( nWordState == SCE_CMAKE_VARIABLE )
                    styler.ColourTo( i, SCE_CMAKE_STRINGVAR);
                bVarInString = false;
            }
            // Covers "${TEST}..."
            else if ( bClassicVarInString && cNextChar == '}' ) {
                styler.ColourTo( i+1, SCE_CMAKE_STRINGVAR);
                bClassicVarInString = false;
            }

            // Start of var in string
            if ( !bIngoreNextDollarSign && cCurrChar == '$' && cNextChar == '{' ) {
                styler.ColourTo( i-1, state);
                bClassicVarInString = true;
                bVarInString = false;
            }
            else if ( !bIngoreNextDollarSign && cCurrChar == '$' ) {
                styler.ColourTo( i-1, state);
                bVarInString = true;
                bClassicVarInString = false;
            }
        }
    }

    // Colourise remaining document
    styler.ColourTo(nLengthDoc-1,state);
}

static void FoldCmakeDoc(Sci_PositionU startPos, Sci_Position length, int, WordList *[], Accessor &styler)
{
    // No folding enabled, no reason to continue...
    if ( styler.GetPropertyInt("fold") == 0 )
        return;

    bool foldAtElse = styler.GetPropertyInt("fold.at.else", 0) == 1;

    Sci_Position lineCurrent = styler.GetLine(startPos);
    Sci_PositionU safeStartPos = styler.LineStart( lineCurrent );

    bool bArg1 = true;
    Sci_Position nWordStart = -1;

    int levelCurrent = SC_FOLDLEVELBASE;
    if (lineCurrent > 0)
        levelCurrent = styler.LevelAt(lineCurrent-1) >> 16;
    int levelNext = levelCurrent;

    for (Sci_PositionU i = safeStartPos; i < startPos + length; i++) {
        char chCurr = styler.SafeGetCharAt(i);

        if ( bArg1 ) {
            if ( nWordStart == -1 && (isCmakeLetter(chCurr)) ) {
                nWordStart = i;
            }
            else if ( isCmakeLetter(chCurr) == false && nWordStart > -1 ) {
                int newLevel = calculateFoldCmake( nWordStart, i-1, levelNext, styler, foldAtElse);

                if ( newLevel == levelNext ) {
                    if ( foldAtElse ) {
                        if ( CmakeNextLineHasElse(i, startPos + length, styler) )
                            levelNext--;
                    }
                }
                else
                    levelNext = newLevel;
                bArg1 = false;
            }
        }

        if ( chCurr == '\n' ) {
            if ( bArg1 && foldAtElse) {
                if ( CmakeNextLineHasElse(i, startPos + length, styler) )
                    levelNext--;
            }

            // If we are on a new line...
            int levelUse = levelCurrent;
            int lev = levelUse | levelNext << 16;
            if (levelUse < levelNext )
                lev |= SC_FOLDLEVELHEADERFLAG;
            if (lev != styler.LevelAt(lineCurrent))
                styler.SetLevel(lineCurrent, lev);

            lineCurrent++;
            levelCurrent = levelNext;
            bArg1 = true; // New line, lets look at first argument again
            nWordStart = -1;
        }
    }

    int levelUse = levelCurrent;
    int lev = levelUse | levelNext << 16;
    if (levelUse < levelNext)
        lev |= SC_FOLDLEVELHEADERFLAG;
    if (lev != styler.LevelAt(lineCurrent))
        styler.SetLevel(lineCurrent, lev);
}

static const char * const cmakeWordLists[] = {
    "Commands",
    "Parameters",
    "UserDefined",
    0,
    0,};

LexerModule lmCmake(SCLEX_CMAKE, ColouriseCmakeDoc, "cmake", FoldCmakeDoc, cmakeWordLists);
