/**
 * 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.apache.weex.el.parse;

import org.apache.weex.WXEnvironment;
import org.apache.weex.utils.WXLogUtils;

import java.util.ArrayList;
import java.util.List;


/**
 * Created by furture on 2017/8/28.
 * simple expression parse, less ast node and save memory
 */
public class Parser {

    private String code;
    private int position;
    private ArrayStack<Token> stacks;
    private ArrayStack<Symbol> operators;

    public Parser(String code){
        this.code = code;
        this.position = 0;
        this.stacks = new ArrayStack<>();
        this.operators = new ArrayStack<>();
    }


    public final Token parse(){
        while (hasNextToken()){
            scanNextToken();
        }
        while (!operators.isEmpty()){
            Symbol op = operators.pop();
            doOperator(op);
        }
        if(stacks.size() == 1){
            return  stacks.pop();
        }
        Block block = new Block(stacks.getList(), Token.TYPE_BLOCK);
        return block;
    }


    /**
     * parse code to ast block.
     * */
    public static Token parse(String code){
        try{
            Parser parser = new Parser(code);
            return parser.parse();
        }catch (Exception e){
            if(WXEnvironment.isApkDebugable()){
                WXLogUtils.e("code " + code, e);
            }
            return new Block(null, Token.TYPE_BLOCK);
        }
    }


    final char scanNextToken(){
        char ch = nextToken();
        if(ch == Operators.DOLLAR){
            position++;
            return ch;
        }else if(Character.isJavaIdentifierStart(ch)){
            scanIdentifier();
        }else if (ch == Operators.BRACKET_START || ch == Operators.BLOCK_START) {
            scanBracket();
        }else if (ch == Operators.ARRAY_START) {
            scanArray();
        }else if (ch ==  Operators.QUOTE || ch == Operators.SINGLE_QUOTE) {
            scanString();
        }else if((ch == Operators.DOT && Character.isDigit(code.charAt(position + 1)))
                || Character.isDigit(ch)){ //number .00 .00e6
            scanNumber();
        }else if(ch ==  Operators.CONDITION_IF){
            scanIf();
        }else if(ch ==  Operators.CONDITION_IF_MIDDLE
                || ch ==  Operators.BRACKET_END
                || ch == Operators.BLOCK_END
                || ch == Operators.SPACE
                || ch == Operators.ARRAY_END){
            position++;
            return ch;
        }else{
            scanOperator();
        }
        return ch;
    }


    final void  scanArray(){
        int stackSize = stacks.size();
        int opSize = operators.size();
        int type = Token.TYPE_IDENTIFIER;
        if(position - 1 < 0 || !Character.isJavaIdentifierPart(code.charAt(position - 1))){
            type = Token.TYPE_ARRAY;
        }
        operators.push(new Symbol(Operators.ARRAY_START_STR, stacks.size()));
        position++;
        while (hasNextToken()){
            char token = scanNextToken();
            if(token == Operators.ARRAY_END){
                break;
            }
        }

        if(stacks.size() <= stackSize){ // empty bracket, none need, save memory
            while (operators.size() > opSize){
                operators.pop();
            }
            return;
        }

        while (operators.size() > opSize){
            Symbol op = operators.pop();
            if(stacks.size() > stackSize){
                doOperator(op);
            }
        }
        List<Token> tokens = new ArrayList<>(4);
        for(int i=stackSize; i<stacks.size(); i++){
            tokens.add(stacks.get(i));
        }
        while (stacks.size() > stackSize){
            stacks.pop();
        }
        if(type == Token.TYPE_ARRAY || stacks.size() == 0){
            Block block = new Block(tokens, Token.TYPE_ARRAY);
            stacks.push(block);
            return;
        }

        Token identifer = stacks.pop();
        Token second = null;
        if(tokens.size() == 1){
            second = tokens.get(0);
        }else{
            second = new Block(tokens, Token.TYPE_BLOCK);
        }
        Operator operator = new Operator(Operators.DOT_STR, type);
        operator.first = identifer;
        operator.second = second;
        stacks.push(operator);
    }

    void  scanBracket(){
        int stackSize = stacks.size();
        int opSize = operators.size();
        if(code.charAt(position) == Operators.BLOCK_START){
            operators.push(new Symbol(Operators.BLOCK_START_STR, stacks.size()));
            position++;
            while (hasNextToken()){
                if(scanNextToken() == Operators.BLOCK_END){
                    break;
                }
            }
        }else{
            operators.push(new Symbol(Operators.BRACKET_START_STR, stacks.size()));
            position++;
            while (hasNextToken()){
                if(scanNextToken() == Operators.BRACKET_END){
                    break;
                }
            }
        }
        if(stacks.size() <= stackSize){ // empty bracket, none need, save memory
            while (operators.size() > opSize){
                operators.pop();
            }
            return;
        }
        while (operators.size() > opSize){
            Symbol op = operators.pop();
            if(stacks.size() > stackSize){
                doOperator(op);
            }
        }
        List<Token> tokens = new ArrayList<>(4);
        for(int i=stackSize; i<stacks.size(); i++){
            tokens.add(stacks.get(i));
        }
        while (stacks.size() > stackSize){
            stacks.pop();
        }
        if(tokens.size() == 1){
            stacks.push(tokens.get(0));
        }else{
            Block block = new Block(tokens, Token.TYPE_BLOCK);
            stacks.push(block);
        }
    }

    /**
     * 1  === (1 + 3) &&  1
     * scan operator
     * */
    void scanOperator(){
        int start = position;
        int length = Math.min(position + 3, code.length());
        String operator = code.substring(position, length);
        if(operator.length() >= 3){
            if(!Operators.OPERATORS_PRIORITY.containsKey(operator)){
                operator = operator.substring(0, 2);
            }
        }
        if(operator.length() >= 2){
            if(!Operators.OPERATORS_PRIORITY.containsKey(operator)){
                operator = operator.substring(0, 1);
            }
        }
        if(!Operators.OPERATORS_PRIORITY.containsKey(operator)){
            //just skip illegal character
            int illegalChar = Math.min(start + 1, code.length());
            WXLogUtils.e("weex", new IllegalArgumentException(code.substring(0, illegalChar) + " illegal code operator" + operator));
            position += operator.length();
            return;
        }
        if((!operators.isEmpty() && operators.peek() != null)){
            String preOp = operators.peek().op;
            if(Operators.OPERATORS_PRIORITY.get(preOp) >= Operators.OPERATORS_PRIORITY.get(operator)){
                Symbol op = operators.pop();
                doOperator(op);
            }
        }
        if(!Operators.isOpEnd(operator)){
            operators.push(new Symbol(operator, stacks.size()));
        }
        position += operator.length();
    }



    void doOperator(Symbol symbol){
        String op = symbol.op;
        if(Operators.BRACKET_START_STR.equals(symbol.op)
                || Operators.BLOCK_START_STR.equals(symbol.op)
                || Operators.ARRAY_START_STR.equals(symbol.op)
                || Operators.DOLLAR_STR.equals(symbol.op)){
            return;
        }
        if(Operators.BLOCK_START_STR.equals(symbol.op)){
            return;
        }
        int second = symbol.pos;
        int first  = Math.max(symbol.pos - 1, 0);
        if(!operators.isEmpty()){
            first = Math.max(first, operators.peek().pos);
        }

        Operator operator = new Operator(op, Token.TYPE_OPERATOR);
        if(Operators.AND_NOT.equals(op)){
            if(stacks.size() > second) {
                Token token = stacks.remove(second);
                operator.self = token;
                stacks.add(second, operator);
                return;
            }
            return; //invalid
        }
        if(stacks.size() > second) {
            operator.second = stacks.remove(second);
        }else{
            return;
        }
        if(stacks.size() > first) {
            operator.first = stacks.remove(first);
        }else{
            if(operator.second == null){
                return;
            }
        }
        stacks.add(first, operator);
    }

    /**
     * condition if
     * */
    void scanIf(){
        Operator operator = new Operator(Operators.CONDITION_IF_STRING, Token.TYPE_OPERATOR);
        int selfIndex = 0;
        doStackOperators(0);
        if(operators.size() > 0){
            selfIndex = Math.max(operators.peek().pos, selfIndex);
        }
        if(stacks.size() > selfIndex){
            operator.self = stacks.pop();
        }

        int stackSize = stacks.size();
        int leftOperatorSize = operators.size();
        position++;
        while (hasNextToken()){
            if(scanNextToken() == Operators.CONDITION_IF_MIDDLE){
                break;
            }
        }
        while (operators.size() > leftOperatorSize){
            Symbol symbol = operators.pop();
            doOperator(symbol);
        }

        while (stacks.size() > stackSize){
            operator.first = stacks.pop();
        }
        int rightOperatorsSize = operators.size();
        while (hasNextToken()){
            scanNextToken();
            if(hasNextToken()){
                scanNextToken();
            }
            if(operators.size() <= rightOperatorsSize){
                break;
            }
        }
        doStackOperators(rightOperatorsSize);
        while (stacks.size() > stackSize){
            operator.second = stacks.pop();
        }
        stacks.push(operator);
    }

    private final void doStackOperators(int operatorSize){
        while (operators.size() > operatorSize){
            Symbol symbol = operators.pop();
            doOperator(symbol);
        }
    }

    /**
     * 1+e6
     * .00
     * .00e6
     * 100
     * 199e5
     * */
    final void scanNumber(){
        boolean isInt = true;
        int start = position;
        if(code.charAt(position) == 'e' || code.charAt(position) == '.'){
            isInt = false;
        }
        position++;
        while (hasNext()){
            char ch = code.charAt(position);
            if(Character.isDigit(ch)
                    || ch == '.'
                    || ch =='e'){
                if(ch == 'e'
                        || ch == '.'){
                    isInt = false;
                }
                position++;
            }else{
                break;
            }
        }
        String number = code.substring(start, position);
        if(".".equals(number)){
            return;
        }
        Token stack = null;
        if(isInt){
            stack = new Token(number, Token.TYPE_INT);
        }else{
            stack = new Token(number, Token.TYPE_DOUBLE);
        }
        stacks.push(stack);
    }


    final void  scanString(){
        int start = position;
        ArrayStack operator = new ArrayStack();
        char quote = code.charAt(start);
        operator.push(quote);
        StringBuilder builder = new StringBuilder();
        for(position= start + 1; position<code.length(); position++){
            char ch = code.charAt(position);
            if(ch == quote){
                if(code.charAt(position -1) != '\\'){
                    operator.pop();
                    if(operator.size() == 0){
                       position++;
                       break;
                    }
                }else{
                    builder.deleteCharAt(builder.length()-1);
                    builder.append(ch);
                }
            }else{
                builder.append(ch);
            }
        }
        String string =  builder.toString();
        Token token = new Token(string, Token.TYPE_STRING);
        stacks.push(token);
    }



    /**
     * scan el expression.
     * item.ddd
     * ${item.dd}
     * $item.dd
     * */
    final void scanIdentifier(){
        int start = position;
        position++;
        while (hasNext()){
            char ch = code.charAt(position);
            if(Character.isJavaIdentifierPart(ch)){
               position++;
            }else{
                break;
            }
            //2.true
        }
        String el = code.substring(start, position);
        if(el.startsWith(Operators.DOLLAR_STR)){
            if(el.length() == Operators.DOLLAR_STR.length()){
                return;
            }
            el = el.substring(Operators.DOLLAR_STR.length());
        }
        int type = Token.TYPE_IDENTIFIER;
        if(Operators.KEYWORDS.containsKey(el)){
            if(!(!operators.isEmpty() && Operators.isDot(operators.peek().op))){
                type = Token.TYPE_KEYWORD;
            }
        }
        Token token = new Token(el, type);
        stacks.push(token);
    }


    final boolean hasNext(){
        return position < code.length();
    }



    final boolean hasNextToken(){
        while (hasNext()){
            char ch = code.charAt(position);
            if(ch == ' '){
                position++;
                continue;
            }
            return  true;
        }
        return false;
    }

    final char nextToken(){
        char ch = code.charAt(position);
        while (ch == ' '){
            position ++;
            if(code.length() <= position){
                break;
            }
            ch = code.charAt(position);
        }
        return ch;
    }
}
