/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.php.twig.editor.lexer;

import java.util.Objects;
import org.netbeans.spi.lexer.LexerInput;
import org.netbeans.spi.lexer.LexerRestartInfo;
import org.netbeans.modules.web.common.api.ByteStack;

@org.netbeans.api.annotations.common.SuppressWarnings({"SF_SWITCH_FALLTHROUGH", "URF_UNREAD_FIELD", "DLS_DEAD_LOCAL_STORE", "DM_DEFAULT_ENCODING"})
%%

%public
%class TwigTopColoringLexer
%type TwigTopTokenId
%function findNextToken
%unicode
%caseless
%char

%eofval{
        if(input.readLength() > 0) {
            // backup eof
            input.backup(1);
            //and return the text as error token
            if (zzLexicalState == ST_BLOCK) {
                return TwigTopTokenId.T_TWIG_BLOCK;
            } else if (zzLexicalState == ST_VAR) {
                return TwigTopTokenId.T_TWIG_VAR;
            } else {
                return TwigTopTokenId.T_HTML;
            }
        } else {
            return null;
        }
%eofval}

%{

    private ByteStack stack = new ByteStack();
    private LexerInput input;
    private Lexing lexing;
    private boolean probablyInDString;
    private boolean probablyInSString;
    private int curlyBalance;

    public TwigTopColoringLexer(LexerRestartInfo info) {
        this.input = info.input();
        if(info.state() != null) {
            //reset state
            setState((LexerState) info.state());
            this.lexing = ((LexerState) info.state()).lexing;
            probablyInDString = ((LexerState) info.state()).probablyInDString;
            probablyInSString = ((LexerState) info.state()).probablyInSString;
            curlyBalance = ((LexerState) info.state()).curlyBalance;
        } else {
            zzState = zzLexicalState = YYINITIAL;
            this.lexing = Lexing.NORMAL;
            probablyInDString = false;
            probablyInSString = false;
            curlyBalance = 0;
            stack.clear();
        }

    }

    private enum Lexing {
        NORMAL,
        RAW,
        VERBATIM;
    }

    public static final class LexerState  {
        final ByteStack stack;
        /** the current state of the DFA */
        final int zzState;
        /** the current lexical state */
        final int zzLexicalState;
        private final Lexing lexing;
        private final boolean probablyInDString;
        private final boolean probablyInSString;
        private final int curlyBalance;

        LexerState(ByteStack stack, int zzState, int zzLexicalState, Lexing lexing, boolean probablyInDString, boolean probablyInSString, int curlyBalance) {
            this.stack = stack;
            this.zzState = zzState;
            this.zzLexicalState = zzLexicalState;
            this.lexing = lexing;
            this.probablyInDString = probablyInDString;
            this.probablyInSString = probablyInSString;
            this.curlyBalance = curlyBalance;
        }

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 17 * hash + Objects.hashCode(this.stack);
            hash = 17 * hash + this.zzState;
            hash = 17 * hash + this.zzLexicalState;
            hash = 17 * hash + Objects.hashCode(this.lexing);
            hash = 17 * hash + (this.probablyInDString ? 1 : 0);
            hash = 17 * hash + (this.probablyInSString ? 1 : 0);
            hash = 17 * hash + this.curlyBalance;
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final LexerState other = (LexerState) obj;
            if (this.zzState != other.zzState) {
                return false;
            }
            if (this.zzLexicalState != other.zzLexicalState) {
                return false;
            }
            if (this.probablyInDString != other.probablyInDString) {
                return false;
            }
            if (this.probablyInSString != other.probablyInSString) {
                return false;
            }
            if (this.curlyBalance != other.curlyBalance) {
                return false;
            }
            if (!Objects.equals(this.stack, other.stack)) {
                return false;
            }
            if (this.lexing != other.lexing) {
                return false;
            }
            return true;
        }
    }

    public LexerState getState() {
        return new LexerState(stack.copyOf(), zzState, zzLexicalState, lexing, probablyInDString, probablyInSString, curlyBalance);
    }

    public void setState(LexerState state) {
        this.stack.copyFrom(state.stack);
        this.zzState = state.zzState;
        this.zzLexicalState = state.zzLexicalState;
        this.lexing = state.lexing;
        this.probablyInDString = state.probablyInDString;
        this.probablyInSString = state.probablyInSString;
        this.curlyBalance = state.curlyBalance;
    }

    protected int getZZLexicalState() {
        return zzLexicalState;
    }

    protected void popState() {
        yybegin(stack.pop());
    }

    protected void pushState(final int state) {
        stack.push(getZZLexicalState());
        yybegin(state);
    }


 // End user code

%}

WHITESPACE=[ \t\r\n]+
BLOCK_START="{%"
BLOCK_END="%}"
BLOCK_RAW_START="{%"[ \t]*"raw"[ \t]*"%}"
BLOCK_RAW_END="{%"[ \t]*"endraw"[ \t]*"%}"
BLOCK_VERBATIM_START="{%"[ \t]*"verbatim"[ \t]*"%}"
BLOCK_VERBATIM_END="{%"[ \t]*"endverbatim"[ \t]*"%}"
VAR_START="{{"
VAR_END="}}"
COMMENT_START="{#"
COMMENT_END=([^#] | #[^}])*"#}"
D_STRING_DELIM=\"
S_STRING_DELIM='
PRECEDED_STRING="\\\'"|"\\\""|"\\\\"
OPEN_CURLY="{"
CLOSE_CURLY="}"

%state ST_RAW_START
%state ST_RAW_END
%state ST_VERBATIM_START
%state ST_VERBATIM_END
%state ST_BLOCK
%state ST_VAR
%state ST_COMMENT
%state ST_HIGHLIGHTING_ERROR

%%

<YYINITIAL, ST_RAW_START, ST_RAW_END, ST_VERBATIM_START, ST_VERBATIM_END, ST_BLOCK, ST_VAR, ST_COMMENT>{WHITESPACE}+ {
}

<YYINITIAL> {
    {BLOCK_RAW_START} {
        if (lexing == Lexing.NORMAL) {
            yypushback(yylength());
            pushState(ST_RAW_START);
        }
    }
    {BLOCK_RAW_END} {
        if (lexing != Lexing.VERBATIM) {
            int indexOfRawBlockStart = yytext().lastIndexOf("{%"); //NOI18N
            yypushback(yylength() - indexOfRawBlockStart);
            pushState(ST_RAW_END);
        }
    }
    {BLOCK_VERBATIM_START} {
        if (lexing == Lexing.NORMAL) {
            yypushback(yylength());
            pushState(ST_VERBATIM_START);
        }
    }
    {BLOCK_VERBATIM_END} {
        if (lexing != Lexing.RAW) {
            int indexOfVerbatimBlockStart = yytext().lastIndexOf("{%"); //NOI18N
            yypushback(yylength() - indexOfVerbatimBlockStart);
            pushState(ST_VERBATIM_END);
        }
    }
    {BLOCK_START} {
        if (lexing == Lexing.NORMAL) {
            if (yylength() > 2) {
                yypushback(2);
                return TwigTopTokenId.T_HTML;
            }
            pushState(ST_BLOCK);
            return TwigTopTokenId.T_TWIG_BLOCK_START;
        }
    }
    {COMMENT_START} {
        if (lexing == Lexing.NORMAL) {
            int textLength = yylength();
            yypushback(2);
            pushState(ST_COMMENT);
            if (textLength > 2) {
                return TwigTopTokenId.T_HTML;
            }
        }
    }
    {VAR_START} {
        if (lexing == Lexing.NORMAL) {
            if (yylength() > 2) {
                yypushback(2);
                return TwigTopTokenId.T_HTML;
            }
            pushState(ST_VAR);
            curlyBalance = 0;
            return TwigTopTokenId.T_TWIG_VAR_START;
        }
    }
    . {}
}

<ST_RAW_START> {
    {BLOCK_START} {
        if (yylength() > 2) {
            yypushback(2);
            return TwigTopTokenId.T_HTML;
        }
        lexing = Lexing.RAW;
        return TwigTopTokenId.T_TWIG_BLOCK_START;
    }
}

<ST_VERBATIM_START> {
    {BLOCK_START} {
        if (yylength() > 2) {
            yypushback(2);
            return TwigTopTokenId.T_HTML;
        }
        lexing = Lexing.VERBATIM;
        return TwigTopTokenId.T_TWIG_BLOCK_START;
    }
}

<ST_RAW_START, ST_VERBATIM_START> {
    {BLOCK_END} {
        if (yylength() > 2) {
            yypushback(2);
            return TwigTopTokenId.T_TWIG_BLOCK;
        }
        popState();
        return TwigTopTokenId.T_TWIG_BLOCK_END;
    }
    . {}
}

<ST_RAW_END, ST_VERBATIM_END> {
    {BLOCK_START} {
        if (yylength() > 2) {
            yypushback(2);
            return TwigTopTokenId.T_HTML;
        }
        lexing = Lexing.NORMAL;
        return TwigTopTokenId.T_TWIG_BLOCK_START;
    }
    {BLOCK_END} {
        if (yylength() > 2) {
            yypushback(2);
            return TwigTopTokenId.T_TWIG_BLOCK;
        }
        popState();
        return TwigTopTokenId.T_TWIG_BLOCK_END;
    }
    . {}
}

<ST_COMMENT> {
    {COMMENT_END} {
        popState();
        return TwigTopTokenId.T_TWIG_COMMENT;
    }
    . {}
}

<ST_BLOCK> {
    {BLOCK_END} {
        if (yylength() > 2) {
            yypushback(2);
            return TwigTopTokenId.T_TWIG_BLOCK;
        }
        popState();
        return TwigTopTokenId.T_TWIG_BLOCK_END;
    }
    . {}
}

<ST_VAR> {
    {PRECEDED_STRING} {
    }
    {D_STRING_DELIM} {
        if (!probablyInSString) {
            probablyInDString = !probablyInDString;
        }
    }
    {S_STRING_DELIM} {
        if (!probablyInDString) {
            probablyInSString = !probablyInSString;
        }
    }
    {OPEN_CURLY} {
        if (!probablyInDString && !probablyInSString) {
            curlyBalance++;
        }
    }
    {CLOSE_CURLY} {
        if (!probablyInDString && !probablyInSString) {
            curlyBalance--;
        }
    }
    "}}}" { // {{{}}}
        if (!probablyInDString && !probablyInSString) {
            if (curlyBalance >= 0 && curlyBalance <= 2) {
                curlyBalance--;
                yypushback(2);
            } else {
                curlyBalance -= 3;
            }
        }
    }
    {VAR_END} {
        if (!probablyInDString && !probablyInSString) {
            if (yylength() > 2) {
                if (curlyBalance == 0 || curlyBalance == 1) {
                    if (zzInput == YYEOF) {
                        yypushback(3);
                    } else {
                        yypushback(2);
                    }
                    return TwigTopTokenId.T_TWIG_VAR;
                }
            }
            if (curlyBalance == 0) {
                popState();
                return TwigTopTokenId.T_TWIG_VAR_END;
            } else if (curlyBalance == 1) {
                // missing closing curly "}"
                popState();
                curlyBalance = 0;
                return TwigTopTokenId.T_TWIG_VAR_END;
            } else {
                curlyBalance -= 2;
            }
        }
    }
    . {}
}

/* ============================================
   Stay in this state until we find a whitespace.
   After we find a whitespace we go the the prev state and try again from the next token.
   ============================================ */
<ST_HIGHLIGHTING_ERROR> {
    . {
        return TwigTopTokenId.T_TWIG_OTHER;
    }
}

/* ============================================
   This rule must be the last in the section!!
   it should contain all the states.
   ============================================ */
<YYINITIAL, ST_RAW_START, ST_RAW_END, ST_VERBATIM_START, ST_VERBATIM_END, ST_BLOCK, ST_VAR, ST_COMMENT> {
    . {
        yypushback(yylength());
        pushState(ST_HIGHLIGHTING_ERROR);
    }
}
