/*
 * Copyright 1999-2017 Alibaba Group Holding Ltd.
 *
 * Licensed 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 com.alibaba.druid.sql.dialect.hive.parser;

import com.alibaba.druid.sql.ast.SQLCurrentTimeExpr;
import com.alibaba.druid.sql.ast.SQLCurrentUserExpr;
import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.expr.*;
import com.alibaba.druid.sql.ast.statement.SQLColumnDefinition;
import com.alibaba.druid.sql.dialect.mysql.ast.expr.MySqlOutFileExpr;
import com.alibaba.druid.sql.parser.*;
import com.alibaba.druid.util.FnvHash;

import java.util.Arrays;

public class HiveExprParser extends SQLExprParser {
    private static final String[] AGGREGATE_FUNCTIONS;
    private static final long[] AGGREGATE_FUNCTIONS_CODES;

    static {
        String[] strings = {"AVG", "COUNT", "MAX", "MIN", "STDDEV", "SUM", "ROW_NUMBER",
                "ROWNUMBER"};

        AGGREGATE_FUNCTIONS_CODES = FnvHash.fnv1a_64_lower(strings, true);
        AGGREGATE_FUNCTIONS = new String[AGGREGATE_FUNCTIONS_CODES.length];
        for (String str : strings) {
            long hash = FnvHash.fnv1a_64_lower(str);
            int index = Arrays.binarySearch(AGGREGATE_FUNCTIONS_CODES, hash);
            AGGREGATE_FUNCTIONS[index] = str;
        }
    }

    public HiveExprParser(String sql) {
        this(new HiveLexer(sql));
        this.lexer.nextToken();
    }

    public HiveExprParser(String sql, SQLParserFeature... features) {
        this(new HiveLexer(sql, features));
        this.lexer.nextToken();
    }

    public HiveExprParser(Lexer lexer) {
        super(lexer);
        this.aggregateFunctions = AGGREGATE_FUNCTIONS;
        this.aggregateFunctionHashCodes = AGGREGATE_FUNCTIONS_CODES;
    }

    @Override
    protected SQLExpr primaryCommon(SQLExpr sqlExpr) {
        sqlExpr = new SQLIdentifierExpr(lexer.stringVal());
        lexer.nextToken();
        return sqlExpr;
    }
    @Override
    protected String doRestSpecific(SQLExpr expr) {
        String name = null;
        if ((lexer.token() == Token.LITERAL_INT || lexer.token() == Token.LITERAL_FLOAT)) {
            name = lexer.numberString();
            lexer.nextToken();
        }
        return name;
    }

    @Override
    protected SQLExpr relationalRestEqeq(SQLExpr expr) {
        Lexer.SavePoint mark = lexer.mark();
        lexer.nextToken();
        SQLExpr rightExp;
        try {
            if (lexer.token() == Token.SEMI) {
                lexer.reset(mark);
                return expr;
            }
            rightExp = bitOr();
        } catch (EOFParserException e) {
            throw new ParserException("EOF, " + expr + "=", e);
        }

        if (lexer.token() == Token.COLONEQ) {
            lexer.nextToken();
            SQLExpr colonExpr = expr();
            rightExp = new SQLBinaryOpExpr(rightExp, SQLBinaryOperator.Assignment, colonExpr, dbType);
        }
        return new SQLBinaryOpExpr(expr, SQLBinaryOperator.Equality, rightExp, dbType);
    }

    @Override
    protected SQLExpr parseAssignItemOnColon(SQLExpr sqlExpr) {
        if (lexer.token() == Token.COLON) {
            lexer.nextToken();
            String str = sqlExpr.toString() + ':';
            str += lexer.numberString();
            lexer.nextToken();
            sqlExpr = new SQLIdentifierExpr(str);
        }
        return sqlExpr;
    }

    @Override
    protected SQLExpr parseSelectItemRest(String ident, long hash_lower) {
        SQLExpr expr = null;
        if (lexer.identifierEquals(FnvHash.Constants.COLLATE)
                && lexer.stringVal().charAt(0) != '`'
        ) {
            lexer.nextToken();
            String collate = lexer.stringVal();
            lexer.nextToken();

            SQLBinaryOpExpr binaryExpr = new SQLBinaryOpExpr(
                    new SQLIdentifierExpr(ident),
                    SQLBinaryOperator.COLLATE,
                    new SQLIdentifierExpr(collate), dbType
            );
            expr = binaryExpr;

        } else if (FnvHash.Constants.TIMESTAMP == hash_lower
                && lexer.stringVal().charAt(0) != '`'
                && lexer.token() == Token.LITERAL_CHARS) {
            String literal = lexer.stringVal();
            lexer.nextToken();

            SQLTimestampExpr ts = new SQLTimestampExpr(literal);
            expr = ts;

            if (lexer.identifierEquals(FnvHash.Constants.AT)) {
                Lexer.SavePoint mark = lexer.mark();
                lexer.nextToken();

                String timeZone = null;
                if (lexer.identifierEquals(FnvHash.Constants.TIME)) {
                    lexer.nextToken();
                    if (lexer.identifierEquals(FnvHash.Constants.ZONE)) {
                        lexer.nextToken();
                        timeZone = lexer.stringVal();
                        lexer.nextToken();
                    }
                }
                if (timeZone == null) {
                    lexer.reset(mark);
                } else {
                    ts.setTimeZone(timeZone);
                }
            }
        } else if (FnvHash.Constants.DATETIME == hash_lower
                && lexer.stringVal().charAt(0) != '`'
                && lexer.token() == Token.LITERAL_CHARS) {
            String literal = lexer.stringVal();
            lexer.nextToken();

            SQLDateTimeExpr ts = new SQLDateTimeExpr(literal);
            expr = ts;
        } else if (FnvHash.Constants.CURRENT_DATE == hash_lower
                && ident.charAt(0) != '`'
                && lexer.token() != Token.LPAREN) {
            expr = new SQLCurrentTimeExpr(SQLCurrentTimeExpr.Type.CURRENT_DATE);

        } else if (FnvHash.Constants.CURRENT_TIMESTAMP == hash_lower
                && ident.charAt(0) != '`'
                && lexer.token() != Token.LPAREN) {
            expr = new SQLCurrentTimeExpr(SQLCurrentTimeExpr.Type.CURRENT_TIMESTAMP);

        } else if (FnvHash.Constants.CURRENT_TIME == hash_lower
                && ident.charAt(0) != '`'
                && lexer.token() != Token.LPAREN) {
            expr = new SQLCurrentTimeExpr(SQLCurrentTimeExpr.Type.CURRENT_TIME);

        } else if (FnvHash.Constants.CURDATE == hash_lower
                && ident.charAt(0) != '`'
                && lexer.token() != Token.LPAREN) {
            expr = new SQLCurrentTimeExpr(SQLCurrentTimeExpr.Type.CURDATE);

        } else if (FnvHash.Constants.LOCALTIME == hash_lower
                && ident.charAt(0) != '`'
                && lexer.token() != Token.LPAREN) {
            expr = new SQLCurrentTimeExpr(SQLCurrentTimeExpr.Type.LOCALTIME);

        } else if (FnvHash.Constants.LOCALTIMESTAMP == hash_lower
                && ident.charAt(0) != '`'
                && lexer.token() != Token.LPAREN) {
            expr = new SQLCurrentTimeExpr(SQLCurrentTimeExpr.Type.LOCALTIMESTAMP);
        }
        return expr;
    }

    public SQLExpr primaryRest(SQLExpr expr) {
        if (lexer.token() == Token.COLON) {
            lexer.nextToken();
            expr = dotRest(expr);
            if (expr instanceof SQLPropertyExpr) {
                SQLPropertyExpr spe = (SQLPropertyExpr) expr;
                spe.setSplitString(":");
            }
            return expr;
        }

        switch (lexer.token()) {
            case LBRACKET:
                SQLArrayExpr array = new SQLArrayExpr();
                array.setExpr(expr);
                lexer.nextToken();
                this.exprList(array.getValues(), array);
                accept(Token.RBRACKET);
                return primaryRest(array);
            case LITERAL_CHARS:
                if (expr instanceof SQLCharExpr) {
                    StringBuilder text2 = new StringBuilder(((SQLCharExpr) expr).getText());
                    do {
                        String chars = lexer.stringVal();
                        text2.append(chars);
                        lexer.nextToken();
                    } while (lexer.token() == Token.LITERAL_CHARS || lexer.token() == Token.LITERAL_ALIAS);
                    expr = new SQLCharExpr(text2.toString());
                }
                break;
            case IDENTIFIER:
                if (lexer.identifierEquals(FnvHash.Constants.BD) && expr instanceof SQLNumericLiteralExpr) {
                    lexer.nextToken();
                    Number num = ((SQLNumericLiteralExpr) expr).getNumber();
                    expr = new SQLDecimalExpr(num.toString());
                }
                break;
            default:
                break;
        }

        return super.primaryRest(expr);
    }

    public SQLExpr primary() {
        final Token tok = lexer.token();
        switch (tok) {
            case WITH: {
                return primaryRest(
                        new SQLQueryExpr(
                                createSelectParser()
                                        .select()));
            }
            case IDENTIFIER:
                final long hash_lower = lexer.hashLCase();
                if (hash_lower == FnvHash.Constants.OUTLINE) {
                    lexer.nextToken();
                    SQLExpr file = primary();
                    SQLExpr expr = new MySqlOutFileExpr(file);

                    return primaryRest(expr);
                }

                SQLCurrentTimeExpr currentTimeExpr = null;
                if (hash_lower == FnvHash.Constants.CURRENT_TIMESTAMP) {
                    currentTimeExpr = new SQLCurrentTimeExpr(SQLCurrentTimeExpr.Type.CURRENT_TIMESTAMP);
                } else if (hash_lower == FnvHash.Constants.CURRENT_DATE) {
                    currentTimeExpr = new SQLCurrentTimeExpr(SQLCurrentTimeExpr.Type.CURRENT_DATE);
                } else if (hash_lower == FnvHash.Constants.CURRENT_USER && isEnabled(SQLParserFeature.EnableCurrentUserExpr)) {
                    lexer.nextToken();
                    return primaryRest(new SQLCurrentUserExpr());
                }

                if (currentTimeExpr != null) {
                    String methodName = lexer.stringVal();
                    lexer.nextToken();

                    if (lexer.token() == Token.LPAREN) {
                        lexer.nextToken();
                        if (lexer.token() == Token.LPAREN) {
                            lexer.nextToken();
                        } else {
                            return primaryRest(
                                    methodRest(new SQLIdentifierExpr(methodName), false)
                            );
                        }
                    }

                    return primaryRest(currentTimeExpr);
                }
            default:
                break;
        }

        return super.primary();
    }

    protected SQLExpr parseAliasExpr(String alias) {
        String chars = alias.substring(1, alias.length() - 1);

        StringBuilder buf = null;

        for (int i = 0; i < chars.length(); ++i) {
            char ch = chars.charAt(i);
            if (ch == '\\' && i < chars.length() - 1) {
                char next = chars.charAt(i + 1);
                if (next == '\\') {
                    if (buf == null) {
                        buf = new StringBuilder();
                        buf.append(chars.substring(0, i));
                    }
                    buf.append('\\');
                    ++i;
                } else if (next == '\"') {
                    if (buf == null) {
                        buf = new StringBuilder();
                        buf.append(chars.substring(0, i));
                    }
                    buf.append('"');
                    ++i;
                } else {
                    if (buf != null) {
                        buf.append(ch);
                    }
                }
            } else {
                if (buf != null) {
                    buf.append(ch);
                }
            }
        }

        if (buf != null) {
            chars = buf.toString();
        }
        return new SQLCharExpr(chars);
    }

    public SQLColumnDefinition parseColumnRest(SQLColumnDefinition column) {
        if (lexer.identifierEquals(FnvHash.Constants.MAPPED)) {
            lexer.nextToken();
            accept(Token.BY);
            this.parseAssignItem(column.getMappedBy(), column);
        }

        if (lexer.identifierEquals(FnvHash.Constants.COLPROPERTIES)) {
            lexer.nextToken();
            this.parseAssignItem(column.getColProperties(), column);
        }

        return super.parseColumnRest(column);
    }

    protected SQLExpr parseInterval() {
        String str = lexer.stringVal();
        accept(Token.INTERVAL);
        if (lexer.token() == Token.AS || lexer.token() == Token.RPAREN) {
            return new SQLIdentifierExpr(str);
        }

        SQLExpr value = expr();

        if (value instanceof SQLIntervalExpr) {
            return value;
        }

        if (lexer.token() != Token.IDENTIFIER) {
            throw new ParserException("Syntax error. " + lexer.info());
        }

        String unit = lexer.stringVal();
        lexer.nextToken();

        SQLIntervalExpr intervalExpr = new SQLIntervalExpr();
        intervalExpr.setValue(value);
        SQLIntervalUnit intervalUnit = SQLIntervalUnit.of(unit.toUpperCase());
        if (intervalUnit == SQLIntervalUnit.YEAR
                && lexer.token() == Token.TO) {
            lexer.nextToken();
            acceptIdentifier("MONTH");
            intervalUnit = SQLIntervalUnit.YEAR_TO_MONTH;
        }
        if (intervalUnit == SQLIntervalUnit.YEAR && lexer.nextIf(Token.TO)) {
            acceptIdentifier(FnvHash.Constants.MONTH);
            intervalUnit = SQLIntervalUnit.YEAR_TO_MONTH;
        } else if (intervalUnit == SQLIntervalUnit.DAY && lexer.nextIf(Token.TO)) {
            acceptIdentifier(FnvHash.Constants.SECOND);
            intervalUnit = SQLIntervalUnit.DAY_HOUR;
        } else if (intervalUnit == SQLIntervalUnit.HOUR && lexer.nextIf(Token.TO)) {
            acceptIdentifier(FnvHash.Constants.SECOND);
            intervalUnit = SQLIntervalUnit.HOUR_SECOND;
        }
        intervalExpr.setUnit(intervalUnit);

        return intervalExpr;
    }

    protected SQLExpr primaryIdentifierRest(long hash_lower, String ident) {
        if (ident.length() > 3 && ident.charAt(0) == '`' && ident.charAt(ident.length() - 1) == '`' && ident.indexOf('.') != -1) {
            return topPropertyExpr(ident);
        }
        return super.primaryIdentifierRest(hash_lower, ident);
    }
}
