package com.chenyang.druid.sql.dialect.sqlserver.parser;

import com.chenyang.druid.DbType;
import com.chenyang.druid.sql.ast.SQLExpr;
import com.chenyang.druid.sql.ast.SQLName;
import com.chenyang.druid.sql.ast.expr.SQLAggregateExpr;
import com.chenyang.druid.sql.ast.expr.SQLAllColumnExpr;
import com.chenyang.druid.sql.ast.expr.SQLBinaryOpExpr;
import com.chenyang.druid.sql.ast.expr.SQLBinaryOperator;
import com.chenyang.druid.sql.ast.expr.SQLCharExpr;
import com.chenyang.druid.sql.ast.expr.SQLIdentifierExpr;
import com.chenyang.druid.sql.ast.expr.SQLIntegerExpr;
import com.chenyang.druid.sql.ast.expr.SQLListExpr;
import com.chenyang.druid.sql.ast.expr.SQLNullExpr;
import com.chenyang.druid.sql.ast.expr.SQLPropertyExpr;
import com.chenyang.druid.sql.ast.expr.SQLSequenceExpr;
import com.chenyang.druid.sql.ast.expr.SQLUnaryExpr;
import com.chenyang.druid.sql.ast.expr.SQLUnaryOperator;
import com.chenyang.druid.sql.ast.expr.SQLVariantRefExpr;
import com.chenyang.druid.sql.ast.statement.SQLColumnDefinition;
import com.chenyang.druid.sql.ast.statement.SQLExprTableSource;
import com.chenyang.druid.sql.ast.statement.SQLSelectItem;
import com.chenyang.druid.sql.ast.statement.SQLUpdateSetItem;
import com.chenyang.druid.sql.dialect.mysql.ast.expr.MySqlIntervalExpr;
import com.chenyang.druid.sql.dialect.sqlserver.ast.SQLServerOutput;
import com.chenyang.druid.sql.dialect.sqlserver.ast.SQLServerTop;
import com.chenyang.druid.sql.dialect.sqlserver.ast.clause.SQLServerOver;
import com.chenyang.druid.sql.dialect.sqlserver.ast.expr.SQLServerDateTimeExpr;
import com.chenyang.druid.sql.dialect.sqlserver.ast.expr.SQLServerObjectReferenceExpr;
import com.chenyang.druid.sql.dialect.sqlserver.ast.stmt.SQLServerUpdateSetItem;
import com.chenyang.druid.sql.parser.Lexer;
import com.chenyang.druid.sql.parser.ParserException;
import com.chenyang.druid.sql.parser.SQLExprParser;
import com.chenyang.druid.sql.parser.SQLParserFeature;
import com.chenyang.druid.sql.parser.Token;
import com.chenyang.druid.util.FnvHash;
import java.util.Arrays;
import java.util.List;

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

   public SQLServerExprParser(Lexer lexer) {
      super(lexer);
      this.dbType = DbType.sqlserver;
      this.aggregateFunctions = AGGREGATE_FUNCTIONS;
      this.aggregateFunctionHashCodes = AGGREGATE_FUNCTIONS_CODES;
   }

   public SQLServerExprParser(String sql) {
      this((Lexer)(new SQLServerLexer(sql)));
      this.lexer.nextToken();
      this.dbType = DbType.sqlserver;
   }

   public SQLServerExprParser(String sql, SQLParserFeature... features) {
      this((Lexer)(new SQLServerLexer(sql, features)));
      this.lexer.nextToken();
      this.dbType = DbType.sqlserver;
   }

   public SQLExpr primary() {
      if (this.lexer.token() == Token.LBRACKET) {
         this.lexer.nextToken();
         SQLExpr name = this.name();
         this.accept(Token.RBRACKET);
         return this.primaryRest(name);
      } else if (this.lexer.token() == Token.MONEY) {
         SQLCharExpr expr = new SQLCharExpr();
         expr.setText(this.lexer.stringVal());
         this.lexer.nextToken();
         return this.primaryRest(expr);
      } else if (this.lexer.token() != Token.PLUS) {
         return super.primary();
      } else {
         this.lexer.nextToken();
         SQLExpr sqlExpr;
         switch (this.lexer.token()) {
            case LITERAL_CHARS:
            case LITERAL_ALIAS:
               sqlExpr = new SQLIdentifierExpr(this.lexer.stringVal());
               sqlExpr = new SQLUnaryExpr(SQLUnaryOperator.Plus, sqlExpr);
               this.lexer.nextToken();
               break;
            case QUES:
               SQLVariantRefExpr variantRefExpr = new SQLVariantRefExpr("?");
               variantRefExpr.setIndex(this.lexer.nextVarIndex());
               sqlExpr = new SQLUnaryExpr(SQLUnaryOperator.Plus, variantRefExpr);
               this.lexer.nextToken();
               break;
            case LITERAL_FLOAT:
               sqlExpr = this.lexer.numberExpr();
               this.lexer.nextToken();
               break;
            case LITERAL_INT:
               sqlExpr = new SQLIntegerExpr(this.lexer.integerValue());
               this.lexer.nextToken();
               break;
            case PLUS:
            case SUB:
            case LPAREN:
            case IDENTIFIER:
            case BANG:
            case CASE:
            case CAST:
            case NULL:
            case INTERVAL:
            case LBRACE:
               sqlExpr = this.primary();

               while(this.lexer.token() == Token.HINT) {
                  this.lexer.nextToken();
               }

               sqlExpr = new SQLUnaryExpr(SQLUnaryOperator.Plus, sqlExpr);
               break;
            default:
               throw new ParserException("TODO " + this.lexer.info());
         }

         SQLExpr expr = this.primaryRest(sqlExpr);
         return expr;
      }
   }

   public SQLServerSelectParser createSelectParser() {
      return new SQLServerSelectParser(this);
   }

   public SQLExpr primaryRest(SQLExpr expr) {
      Token token = this.lexer.token();
      if (token == Token.DOTDOT) {
         expr = this.nameRest((SQLName)expr);
      } else if (this.lexer.identifierEquals(FnvHash.Constants.VALUE) && expr instanceof SQLIdentifierExpr) {
         SQLIdentifierExpr identExpr = (SQLIdentifierExpr)expr;
         if (identExpr.nameHashCode64() == FnvHash.Constants.NEXT) {
            this.lexer.nextToken();
            this.accept(Token.FOR);
            SQLName name = this.name();
            SQLSequenceExpr seq = new SQLSequenceExpr();
            seq.setSequence(name);
            seq.setFunction(SQLSequenceExpr.Function.NextVal);
            expr = seq;
         }
      } else if (this.lexer.identifierEquals(FnvHash.Constants.COLLATE)) {
         this.lexer.nextToken();
         if (this.lexer.token() == Token.EQ) {
            this.lexer.nextToken();
         }

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

         String collate = this.lexer.stringVal();
         this.lexer.nextToken();
         SQLBinaryOpExpr binaryExpr = new SQLBinaryOpExpr(expr, SQLBinaryOperator.COLLATE, new SQLIdentifierExpr(collate), DbType.sqlserver);
         return this.primaryRest(binaryExpr);
      }

      return super.primaryRest(expr);
   }

   protected SQLExpr dotRest(SQLExpr expr) {
      boolean backet = false;
      if (this.lexer.token() == Token.LBRACKET) {
         this.lexer.nextToken();
         backet = true;
      }

      expr = super.dotRest(expr);
      if (backet) {
         this.accept(Token.RBRACKET);
      }

      return expr;
   }

   public SQLName nameRest(SQLName expr) {
      if (this.lexer.token() == Token.DOTDOT) {
         this.lexer.nextToken();
         boolean backet = false;
         if (this.lexer.token() == Token.LBRACKET) {
            this.lexer.nextToken();
            backet = true;
         }

         String text = this.lexer.stringVal();
         this.lexer.nextToken();
         if (backet) {
            this.accept(Token.RBRACKET);
         }

         SQLServerObjectReferenceExpr owner = new SQLServerObjectReferenceExpr(expr);
         expr = new SQLPropertyExpr(owner, text);
      }

      return super.nameRest(expr);
   }

   public SQLServerTop parseTop() {
      if (this.lexer.token() == Token.TOP) {
         SQLServerTop top = new SQLServerTop();
         this.lexer.nextToken();
         boolean paren = false;
         if (this.lexer.token() == Token.LPAREN) {
            paren = true;
            this.lexer.nextToken();
         }

         if (this.lexer.token() == Token.LITERAL_INT) {
            top.setExpr(this.lexer.integerValue().intValue());
            this.lexer.nextToken();
         } else {
            top.setExpr(this.primary());
         }

         if (paren) {
            this.accept(Token.RPAREN);
         }

         if (this.lexer.token() == Token.PERCENT) {
            this.lexer.nextToken();
            top.setPercent(true);
         }

         if (this.lexer.token() == Token.WITH) {
            this.accept(Token.WITH);
            this.accept(Token.TIES);
            top.setWithTies(true);
         }

         return top;
      } else {
         return null;
      }
   }

   protected SQLServerOutput parserOutput() {
      if (!this.lexer.identifierEquals("OUTPUT")) {
         return null;
      } else {
         this.lexer.nextToken();
         SQLServerOutput output = new SQLServerOutput();
         List<SQLSelectItem> selectList = output.getSelectList();

         while(true) {
            SQLSelectItem selectItem = this.parseSelectItem();
            selectList.add(selectItem);
            if (this.lexer.token() != Token.COMMA) {
               if (this.lexer.token() == Token.INTO) {
                  this.lexer.nextToken();
                  output.setInto(new SQLExprTableSource(this.name()));
                  if (this.lexer.token() == Token.LPAREN) {
                     this.lexer.nextToken();
                     this.exprList(output.getColumns(), output);
                     this.accept(Token.RPAREN);
                  }
               }

               return output;
            }

            this.lexer.nextToken();
         }
      }
   }

   public SQLSelectItem parseSelectItem() {
      SQLExpr expr;
      if (this.lexer.token() == Token.IDENTIFIER) {
         expr = new SQLIdentifierExpr(this.lexer.stringVal());
         this.lexer.nextTokenComma();
         if (this.lexer.token() != Token.COMMA) {
            expr = this.primaryRest(expr);
            expr = this.exprRest(expr);
         }

         while(this.lexer.token() == Token.AT) {
            expr = this.parseDateTimeExpr(expr);
         }
      } else {
         expr = this.expr();
      }

      String alias = this.as();
      return new SQLSelectItem(expr, alias);
   }

   public SQLColumnDefinition createColumnDefinition() {
      SQLColumnDefinition column = new SQLColumnDefinition();
      column.setDbType(this.dbType);
      return column;
   }

   public SQLColumnDefinition parseColumnRest(SQLColumnDefinition column) {
      if (this.lexer.token() == Token.IDENTITY) {
         this.lexer.nextToken();
         SQLColumnDefinition.Identity identity = new SQLColumnDefinition.Identity();
         if (this.lexer.token() == Token.LPAREN) {
            this.lexer.nextToken();
            SQLIntegerExpr seed = (SQLIntegerExpr)this.primary();
            this.accept(Token.COMMA);
            SQLIntegerExpr increment = (SQLIntegerExpr)this.primary();
            this.accept(Token.RPAREN);
            identity.setSeed((Integer)seed.getNumber());
            identity.setIncrement((Integer)increment.getNumber());
         }

         if (this.lexer.token() == Token.NOT) {
            this.lexer.nextToken();
            if (this.lexer.token() == Token.NULL) {
               this.lexer.nextToken();
               column.setDefaultExpr(new SQLNullExpr());
            } else {
               this.accept(Token.FOR);
               this.acceptIdentifier("REPLICATION ");
               identity.setNotForReplication(true);
            }
         }

         column.setIdentity(identity);
      }

      return super.parseColumnRest(column);
   }

   public SQLUpdateSetItem parseUpdateSetItem() {
      SQLServerUpdateSetItem item = new SQLServerUpdateSetItem();
      if (this.lexer.token() == Token.LPAREN) {
         this.lexer.nextToken();
         SQLListExpr list = new SQLListExpr();
         this.exprList(list.getItems(), list);
         this.accept(Token.RPAREN);
         item.setColumn(list);
      } else {
         Token token = this.lexer.token();
         long hash;
         String identName;
         if (token == Token.IDENTIFIER) {
            identName = this.lexer.stringVal();
            hash = this.lexer.hash_lower();
         } else if (token == Token.LITERAL_CHARS) {
            identName = '\'' + this.lexer.stringVal() + '\'';
            hash = 0L;
         } else {
            identName = this.lexer.stringVal();
            hash = 0L;
         }

         this.lexer.nextTokenOperator();

         SQLExpr expr;
         String propertyName;
         for(expr = new SQLIdentifierExpr(identName, hash); this.lexer.token() == Token.DOT; expr = new SQLPropertyExpr(expr, propertyName)) {
            this.lexer.nextToken();
            propertyName = this.lexer.stringVal();
            this.lexer.nextTokenEq();
         }

         item.setColumn(expr);
      }

      if (this.lexer.token() == Token.COLONEQ) {
         item.setOperator(SQLBinaryOperator.Assignment);
      } else if (this.lexer.token() == Token.EQ) {
         item.setOperator(SQLBinaryOperator.Equality);
      } else if (this.lexer.token() == Token.AddAssignment) {
         item.setOperator(SQLBinaryOperator.AddAssignment);
      } else if (this.lexer.token() == Token.SubtractAssignment) {
         item.setOperator(SQLBinaryOperator.SubtractAssignment);
      } else if (this.lexer.token() == Token.MultiplyAssignment) {
         item.setOperator(SQLBinaryOperator.MultiplyAssignment);
      } else if (this.lexer.token() == Token.DivideAssignment) {
         item.setOperator(SQLBinaryOperator.DivideAssignment);
      } else if (this.lexer.token() == Token.BitwiseAndAssignment) {
         item.setOperator(SQLBinaryOperator.BitwiseAndAssignment);
      } else if (this.lexer.token() == Token.BitwiseOrAssignment) {
         item.setOperator(SQLBinaryOperator.BitwiseOrAssignment);
      } else if (this.lexer.token() == Token.ModulusAssignment) {
         item.setOperator(SQLBinaryOperator.ModulusAssignment);
      } else {
         if (this.lexer.token() != Token.CARETEQ) {
            throw new ParserException("syntax error, expect EQ, actual " + this.lexer.token() + " " + this.lexer.info());
         }

         item.setOperator(SQLBinaryOperator.BitwiseXorEQ);
      }

      this.lexer.nextTokenValue();
      item.setValue(this.expr());
      return item;
   }

   public SQLExpr expr() {
      if (this.lexer.token() == Token.STAR) {
         this.lexer.nextToken();
         SQLExpr expr = new SQLAllColumnExpr();
         if (this.lexer.token() == Token.DOT) {
            this.lexer.nextToken();
            this.accept(Token.STAR);
            return new SQLPropertyExpr(expr, "*");
         } else {
            return expr;
         }
      } else {
         if (this.dbType == DbType.mysql && this.lexer.token() == Token.INTERVAL) {
            this.lexer.mark();
            this.lexer.nextToken();
            if (this.lexer.token() != Token.LPAREN) {
               MySqlIntervalExpr interval = new MySqlIntervalExpr();
               SQLExpr value = this.expr();
               SQLExpr unit = this.expr();
               interval.setUnit(unit);
               interval.setValue(value);
               return interval;
            }

            this.lexer.reset();
         }

         SQLExpr expr;
         for(expr = this.primary(); this.lexer.token() == Token.AT; expr = this.parseDateTimeExpr(expr)) {
         }

         Token token = this.lexer.token();
         if (token == Token.COMMA) {
            return expr;
         } else if (token == Token.EQ) {
            expr = this.relationalRest(expr);
            expr = this.andRest(expr);
            expr = this.xorRest(expr);
            expr = this.orRest(expr);
            return expr;
         } else {
            return this.exprRest(expr);
         }
      }
   }

   public SQLExpr parseDateTimeExpr(SQLExpr expr) {
      SQLServerDateTimeExpr timeExpr = new SQLServerDateTimeExpr();
      timeExpr.setInputdate(expr);
      this.accept(Token.AT);
      this.acceptIdentifier(Token.TIME.name);
      this.acceptIdentifier(Token.ZONE.name);
      if (this.lexer.token() == Token.LITERAL_CHARS) {
         SQLCharExpr value = new SQLCharExpr(this.lexer.stringVal());
         timeExpr.setValue(value);
         this.lexer.nextToken();
      } else {
         this.printError(this.lexer.token());
      }

      return timeExpr;
   }

   protected void over(SQLAggregateExpr aggregateExpr) {
      this.lexer.nextToken();
      if (this.lexer.token() != Token.LPAREN) {
         SQLName overRef = this.name();
         aggregateExpr.setOverRef(overRef);
      } else {
         SQLServerOver over = new SQLServerOver();
         this.parseReferenceWindowName(over);
         this.over(over);
         aggregateExpr.setOver(over);
      }
   }

   public void parseReferenceWindowName(SQLServerOver over) {
      this.lexer.mark();
      this.lexer.nextToken();
      if (this.lexer.token() == Token.IDENTIFIER && !this.lexer.identifierEquals(FnvHash.Constants.ROWS) && !this.lexer.identifierEquals(FnvHash.Constants.RANGE) && !this.lexer.identifierEquals(FnvHash.Constants.GROUPS)) {
         this.lexer.mark();
         SQLExpr name = this.expr();
         over.setReferenceWindowName(name);
      }

      this.lexer.reset();
   }

   static {
      String[] strings = new String[]{"AVG", "COUNT", "FIRST_VALUE", "MAX", "MIN", "ROW_NUMBER", "STDDEV", "SUM", "PERCENTILE_CONT", "PERCENTILE_DISC", "LISTAGG", "STRING_AGG"};
      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;
      }

   }
}
