package com.alibaba.druid.sql.parser;

import com.alibaba.druid.DbType;
import com.alibaba.druid.sql.ast.statement.SQLCreateTableStatement;
import com.alibaba.druid.util.FnvHash;
import com.alibaba.druid.util.StringUtils;

import java.util.TimeZone;

public class SQLParser {
    protected final Lexer lexer;
    protected DbType dbType;
    private int errorEndPos;

    public SQLParser(String sql, DbType dbType, SQLParserFeature... features) {
        this(new Lexer(sql, (Lexer.CommentHandler) null, dbType), dbType);

        for (SQLParserFeature feature : features) {
            this.config(feature, true);
        }

        this.lexer.nextToken();
    }

    public SQLParser(String sql) {
        this(sql, (DbType) null);
    }

    public SQLParser(Lexer lexer) {
        this(lexer, (DbType) null);
        if (this.dbType == null) {
            this.dbType = lexer.dbType;
        }

    }

    public SQLParser(Lexer lexer, DbType dbType) {
        this.errorEndPos = -1;
        this.lexer = lexer;
        this.dbType = dbType;
    }

    public final Lexer getLexer() {
        return this.lexer;
    }

    public DbType getDbType() {
        return this.dbType;
    }

    protected boolean identifierEquals(String text) {
        return this.lexer.identifierEquals(text);
    }

    protected void acceptIdentifier(String text) {
        if (this.lexer.identifierEquals(text)) {
            this.lexer.nextToken();
        } else {
            this.setErrorEndPos(this.lexer.pos());
            throw new ParserException("syntax error, expect " + text + ", actual " + this.lexer.token + ", " + this.lexer.info());
        }
    }

    protected String tableAlias() {
        return this.tableAlias(false);
    }

    protected String tableAlias(boolean must) {
        Token token = this.lexer.token;
        if (token != Token.CONNECT && token != Token.START && token != Token.SELECT && token != Token.FROM && token != Token.WHERE) {
            if (token == Token.IDENTIFIER) {
                String ident = this.lexer.stringVal;
                long hash = this.lexer.hash_lower;
                if (this.isEnabled(SQLParserFeature.IgnoreNameQuotes) && ident.length() > 1) {
                    ident = StringUtils.removeNameQuotes(ident);
                }

                if (hash == FnvHash.Constants.START || hash == FnvHash.Constants.CONNECT || hash == FnvHash.Constants.NATURAL || hash == FnvHash.Constants.CROSS || hash == FnvHash.Constants.OFFSET || hash == FnvHash.Constants.LIMIT) {
                    if (must) {
                        throw new ParserException("illegal alias. " + this.lexer.info());
                    }

                    Lexer.SavePoint mark = this.lexer.mark();
                    this.lexer.nextToken();
                    switch (this.lexer.token) {
                        case EOF:
                        case COMMA:
                        case WHERE:
                        case INNER:
                        case ON:
                            return ident;
                        default:
                            this.lexer.reset(mark);
                            return null;
                    }
                }

                if (!must) {
                    if (hash == FnvHash.Constants.MODEL) {
                        Lexer.SavePoint mark = this.lexer.mark();
                        this.lexer.nextToken();
                        if (this.lexer.token != Token.PARTITION && this.lexer.token != Token.UNION && !this.lexer.identifierEquals(FnvHash.Constants.DIMENSION) && !this.lexer.identifierEquals(FnvHash.Constants.IGNORE) && !this.lexer.identifierEquals(FnvHash.Constants.KEEP)) {
                            return ident;
                        } else {
                            this.lexer.reset(mark);
                            return null;
                        }
                    }

                    if (hash == FnvHash.Constants.WINDOW) {
                        Lexer.SavePoint mark = this.lexer.mark();
                        this.lexer.nextToken();
                        if (this.lexer.token == Token.IDENTIFIER) {
                            this.lexer.reset(mark);
                            return null;
                        }

                        return ident;
                    }

                    if (hash == FnvHash.Constants.DISTRIBUTE || hash == FnvHash.Constants.SORT || hash == FnvHash.Constants.CLUSTER) {
                        Lexer.SavePoint mark = this.lexer.mark();
                        this.lexer.nextToken();
                        if (this.lexer.token == Token.BY) {
                            this.lexer.reset(mark);
                            return null;
                        }

                        return ident;
                    }

                    if (hash == FnvHash.Constants.ASOF && this.dbType == DbType.clickhouse) {
                        Lexer.SavePoint mark = this.lexer.mark();
                        this.lexer.nextToken();
                        if (this.lexer.token != Token.LEFT && this.lexer.token != Token.JOIN) {
                            return ident;
                        }

                        this.lexer.reset(mark);
                        return null;
                    }
                }
            }

            if (!must) {
                switch (token) {
                    case INNER:
                    case LEFT:
                    case RIGHT:
                    case FULL: {
                        Lexer.SavePoint mark = this.lexer.mark();
                        String strVal = this.lexer.stringVal();
                        this.lexer.nextToken();
                        if (this.lexer.token != Token.OUTER && this.lexer.token != Token.JOIN && !this.lexer.identifierEquals(FnvHash.Constants.ANTI) && !this.lexer.identifierEquals(FnvHash.Constants.SEMI)) {
                            return strVal;
                        }

                        this.lexer.reset(mark);
                        break;
                    }
                    case ON:
                    case RPAREN:
                    case FROM:
                    case GROUP:
                    default:
                        break;
                    case IN: {
                        Lexer.SavePoint mark = lexer.mark();
                        String strVal = lexer.stringVal();
                        lexer.nextToken();
                        switch (lexer.token) {
                            case WHERE:
                            case RPAREN: {
                                return strVal;
                            }
                            default:
                                lexer.reset(mark);
                                break;
                        }
                        break;
                    }
                    case FOR:
                    case GRANT:
                    case CHECK: {
                        if (this.dbType == DbType.odps || this.dbType == DbType.hive) {
                            String strVal = this.lexer.stringVal();
                            this.lexer.nextToken();
                            return strVal;
                        }
                        break;
                    }
                    case TABLE: {
                        if (this.dbType == DbType.odps) {
                            Lexer.SavePoint mark = this.lexer.mark();
                            String strVal = this.lexer.stringVal();
                            this.lexer.nextToken();
                            switch (this.lexer.token) {
                                case FROM:
                                case GROUP:
                                    return strVal;
                                default:
                                    this.lexer.reset(mark);
                            }
                        }
                        break;
                    }
                    case SHOW:
                    case REPEAT:
                    case USE:
                    case OUT: {
                        String strVal = this.lexer.stringVal();
                        this.lexer.nextToken();
                        return strVal;
                    }
                }
            }

            return this.as();
        } else if (must) {
            throw new ParserException("illegal alias. " + this.lexer.info());
        } else {
            return null;
        }
    }

    protected String as() {
        String alias = null;
        Token token = this.lexer.token;
        if (token == Token.COMMA) {
            return null;
        } else if (token == Token.AS) {
            this.lexer.nextTokenAlias();
            if (this.lexer.token == Token.LPAREN) {
                return null;
            } else if (this.dbType != DbType.oracle || this.lexer.token != Token.COMMA && this.lexer.token != Token.FROM) {
                alias = this.lexer.stringVal();
                this.lexer.nextToken();
                if (alias == null) {
                    if (this.lexer.token == Token.LPAREN) {
                        return null;
                    } else {
                        throw new ParserException("Error : " + this.lexer.info());
                    }
                } else {
                    while (this.lexer.token == Token.DOT) {
                        this.lexer.nextToken();
                        alias = alias + '.' + this.lexer.token.name();
                        this.lexer.nextToken();
                    }

                    return alias;
                }
            } else {
                return null;
            }
        } else {
            if (this.lexer.token == Token.LITERAL_ALIAS) {
                alias = this.lexer.stringVal();
                this.lexer.nextToken();
            } else if (this.lexer.token == Token.IDENTIFIER) {
                alias = this.lexer.stringVal();
                this.lexer.nextToken();
            } else if (this.lexer.token == Token.LITERAL_CHARS) {
                alias = "'" + this.lexer.stringVal() + "'";
                this.lexer.nextToken();
            } else {
                switch (this.lexer.token) {
                    case INNER:
                    case CHECK:
                    case SHOW:
                    case INDEX:
                    case ALL:
                    case CLOSE:
                    case VALUES:
                    case SEQUENCE:
                    case TO:
                        if (this.dbType == DbType.odps || this.dbType == DbType.hive) {
                            alias = this.lexer.stringVal();
                            this.lexer.nextToken();
                        }
                    case ON:
                    case RPAREN:
                    case FROM:
                    case LEFT:
                    case RIGHT:
                    case IN:
                    case FOR:
                    case GRANT:
                    case TABLE:
                    case REPEAT:
                    case USE:
                    case OUT:
                    default:
                        break;
                    case GROUP:
                    case ORDER:
                    case DEFAULT:
                        if (this.dbType == DbType.odps || this.dbType == DbType.hive) {
                            Lexer.SavePoint mark = this.lexer.mark();
                            alias = this.lexer.stringVal();
                            this.lexer.nextToken();
                            if (this.lexer.token == Token.BY) {
                                this.lexer.reset(mark);
                                alias = null;
                            }
                        }
                        break;
                    case FULL:
                    case CASE:
                    case USER:
                    case LOB:
                    case END:
                    case DEFERRED:
                    case OUTER:
                    case DO:
                    case LOOP:
                    case STORE:
                    case MOD:
                    case ANY:
                    case BEGIN:
                    case CAST:
                    case COMPUTE:
                    case ESCAPE:
                    case MERGE:
                    case OPEN:
                    case SOME:
                    case TRUNCATE:
                    case UNTIL:
                    case VIEW:
                    case KILL:
                    case COMMENT:
                    case TABLESPACE:
                        alias = this.lexer.stringVal();
                        this.lexer.nextToken();
                        break;
                    case INTERSECT:
                    case EXCEPT:
                    case DESC:
                    case MINUS:
                        alias = this.lexer.stringVal();
                        Lexer.SavePoint mark = this.lexer.mark();
                        this.lexer.nextToken();
                        if (this.lexer.token() != Token.COMMA && this.lexer.token() != Token.RPAREN) {
                            alias = null;
                            this.lexer.reset(mark);
                        }
                }
            }

            switch (this.lexer.token) {
                case KEY:
                case INTERVAL:
                case CONSTRAINT:
                    alias = this.lexer.token.name();
                    this.lexer.nextToken();
                    return alias;
                default:
                    if (this.isEnabled(SQLParserFeature.IgnoreNameQuotes) && alias != null && alias.length() > 1) {
                        alias = StringUtils.removeNameQuotes(alias);
                    }

                    return alias;
            }
        }
    }

    protected String alias() {
        String alias = null;
        if (this.lexer.token == Token.LITERAL_ALIAS) {
            alias = this.lexer.stringVal();
            this.lexer.nextToken();
        } else if (this.lexer.token == Token.IDENTIFIER) {
            alias = this.lexer.stringVal();
            this.lexer.nextToken();
        } else if (this.lexer.token == Token.LITERAL_CHARS) {
            alias = "'" + this.lexer.stringVal() + "'";
            this.lexer.nextToken();
        } else {
            switch (this.lexer.token) {
                case INNER:
                case GROUP:
                case LEFT:
                case RIGHT:
                case FULL:
                case IN:
                case GRANT:
                case CHECK:
                case TABLE:
                case SHOW:
                case REPEAT:
                case OUT:
                case CASE:
                case USER:
                case LOB:
                case END:
                case DEFERRED:
                case DO:
                case LOOP:
                case STORE:
                case ANY:
                case BEGIN:
                case CAST:
                case COMPUTE:
                case ESCAPE:
                case MERGE:
                case OPEN:
                case SOME:
                case TRUNCATE:
                case UNTIL:
                case VIEW:
                case KILL:
                case COMMENT:
                case TABLESPACE:
                case INTERSECT:
                case EXCEPT:
                case DESC:
                case MINUS:
                case INDEX:
                case ALL:
                case CLOSE:
                case VALUES:
                case SEQUENCE:
                case TO:
                case ORDER:
                case DEFAULT:
                case KEY:
                case INTERVAL:
                case CONSTRAINT:
                case PCTFREE:
                case INITRANS:
                case MAXTRANS:
                case SEGMENT:
                case CREATION:
                case IMMEDIATE:
                case STORAGE:
                case NEXT:
                case MINEXTENTS:
                case MAXEXTENTS:
                case MAXSIZE:
                case PCTINCREASE:
                case FLASH_CACHE:
                case CELL_FLASH_CACHE:
                case NONE:
                case ROW:
                case CHUNK:
                case CACHE:
                case NOCACHE:
                case LOGGING:
                case NOCOMPRESS:
                case KEEP_DUPLICATES:
                case EXCEPTIONS:
                case PURGE:
                case INITIALLY:
                case ENABLE:
                case DISABLE:
                case ANALYZE:
                case OPTIMIZE:
                case REVOKE:
                case NEW:
                case LOCK:
                case LIMIT:
                case IDENTIFIED:
                case PASSWORD:
                case BINARY:
                case WINDOW:
                case OFFSET:
                case SHARE:
                case START:
                case CONNECT:
                case MATCHED:
                case ERRORS:
                case REJECT:
                case UNLIMITED:
                case EXCLUSIVE:
                case MODE:
                case ADVISE:
                case TYPE:
                case FUNCTION:
                case NULL:
                case SCHEMA:
                case PARTITION:
                case UPDATE:
                case LIKE:
                case IS:
                case UNIQUE:
                case INOUT:
                case DECLARE:
                case TRIGGER:
                case BY:
                    alias = this.lexer.stringVal();
                    this.lexer.nextToken();
                    return alias;
                case ON:
                case RPAREN:
                case FROM:
                case FOR:
                case USE:
                case OUTER:
                case MOD:
                default:
                    alias = this.lexer.stringVal();
                    this.lexer.nextToken();
                    return alias;
                case QUES:
                    alias = "?";
                    this.lexer.nextToken();
            }
        }

        return alias;
    }

    protected void printError(Token token) {
        String arround;
        if (this.lexer.mark >= 0 && this.lexer.text.length() > this.lexer.mark + 30) {
            int begin;
            int end;
            if (this.lexer.mark - 5 > 0) {
                begin = this.lexer.mark - 5;
                end = this.lexer.mark + 30;
            } else {
                begin = this.lexer.mark;
                end = this.lexer.mark + 30;
            }

            if (begin < 10) {
                begin = 0;
            } else {
                for (int i = 1; i < 10 && i < begin; ++i) {
                    char ch = this.lexer.text.charAt(begin - i);
                    if (ch == ' ' || ch == '\n') {
                        begin = begin - i + 1;
                    }
                }
            }

            arround = this.lexer.text.substring(begin, end);
        } else if (this.lexer.mark >= 0) {
            if (this.lexer.mark - 5 > 0) {
                arround = this.lexer.text.substring(this.lexer.mark - 5);
            } else {
                arround = this.lexer.text.substring(this.lexer.mark);
            }
        } else {
            arround = this.lexer.text;
        }

        StringBuilder buf = (new StringBuilder()).append("syntax error, error in :'").append(arround);
        if (token != this.lexer.token) {
            buf.append("', expect ").append(token.name).append(", actual ").append(this.lexer.token.name);
        }

        buf.append(", ").append(this.lexer.info());
        throw new ParserException(buf.toString());
    }

    public void acceptIf(Token token) {
        if (this.lexer.token == token) {
            this.accept(token);
        }

    }

    public void accept(Token token) {
        if (this.lexer.token == token) {
            this.lexer.nextToken();
        } else {
            this.setErrorEndPos(this.lexer.pos());
            this.printError(token);
        }

    }

    public int acceptInteger() {
        if (this.lexer.token == Token.LITERAL_INT) {
            int intVal = (Integer) this.lexer.integerValue();
            this.lexer.nextToken();
            return intVal;
        } else {
            throw new ParserException("syntax error, expect int, actual " + this.lexer.token + " " + this.lexer.info());
        }
    }

    public void match(Token token) {
        if (this.lexer.token != token) {
            throw new ParserException("syntax error, expect " + token + ", actual " + this.lexer.token + " " + this.lexer.info());
        }
    }

    protected void setErrorEndPos(int errPos) {
        if (errPos > this.errorEndPos) {
            this.errorEndPos = errPos;
        }

    }

    public void config(SQLParserFeature feature, boolean state) {
        this.lexer.config(feature, state);
    }

    public TimeZone getTimeZone() {
        return this.lexer.getTimeZone();
    }

    public void setTimeZone(TimeZone timeZone) {
        this.lexer.setTimeZone(timeZone);
    }

    public final boolean isEnabled(SQLParserFeature feature) {
        return this.lexer.isEnabled(feature);
    }

    protected SQLCreateTableStatement newCreateStatement() {
        return new SQLCreateTableStatement(this.dbType);
    }
}
