package util.sqlparse.visitor.oracle.visitor;

import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.SQLLimit;
import com.alibaba.druid.sql.ast.SQLName;
import com.alibaba.druid.sql.ast.SQLObject;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.ast.expr.SQLBetweenExpr;
import com.alibaba.druid.sql.ast.expr.SQLBigIntExpr;
import com.alibaba.druid.sql.ast.expr.SQLBinaryExpr;
import com.alibaba.druid.sql.ast.expr.SQLBinaryOpExpr;
import com.alibaba.druid.sql.ast.expr.SQLBooleanExpr;
import com.alibaba.druid.sql.ast.expr.SQLCaseExpr;
import com.alibaba.druid.sql.ast.expr.SQLCharExpr;
import com.alibaba.druid.sql.ast.expr.SQLDateExpr;
import com.alibaba.druid.sql.ast.expr.SQLDateTimeExpr;
import com.alibaba.druid.sql.ast.expr.SQLDecimalExpr;
import com.alibaba.druid.sql.ast.expr.SQLDoubleExpr;
import com.alibaba.druid.sql.ast.expr.SQLExistsExpr;
import com.alibaba.druid.sql.ast.expr.SQLFloatExpr;
import com.alibaba.druid.sql.ast.expr.SQLHexExpr;
import com.alibaba.druid.sql.ast.expr.SQLInListExpr;
import com.alibaba.druid.sql.ast.expr.SQLInSubQueryExpr;
import com.alibaba.druid.sql.ast.expr.SQLIntegerExpr;
import com.alibaba.druid.sql.ast.expr.SQLJSONExpr;
import com.alibaba.druid.sql.ast.expr.SQLMethodInvokeExpr;
import com.alibaba.druid.sql.ast.expr.SQLNCharExpr;
import com.alibaba.druid.sql.ast.expr.SQLNullExpr;
import com.alibaba.druid.sql.ast.expr.SQLNumberExpr;
import com.alibaba.druid.sql.ast.expr.SQLRealExpr;
import com.alibaba.druid.sql.ast.expr.SQLSmallIntExpr;
import com.alibaba.druid.sql.ast.expr.SQLTimeExpr;
import com.alibaba.druid.sql.ast.expr.SQLTimestampExpr;
import com.alibaba.druid.sql.ast.expr.SQLTinyIntExpr;
import com.alibaba.druid.sql.ast.expr.SQLUnaryExpr;
import com.alibaba.druid.sql.ast.expr.SQLValuableExpr;
import com.alibaba.druid.sql.ast.expr.SQLVariantRefExpr;
import com.alibaba.druid.sql.ast.statement.SQLInsertStatement;
import com.alibaba.druid.sql.ast.statement.SQLMergeStatement;
import com.alibaba.druid.sql.ast.statement.SQLReplaceStatement;
import com.alibaba.druid.sql.ast.statement.SQLSelect;
import com.alibaba.druid.sql.ast.statement.SQLSelectItem;
import com.alibaba.druid.sql.ast.statement.SQLUpdateSetItem;
import com.alibaba.druid.sql.dialect.oracle.ast.stmt.OracleInsertStatement;
import com.alibaba.druid.sql.dialect.oracle.ast.stmt.OracleMultiInsertStatement;
import java.util.ArrayList;
import java.util.List;
import util.sqlparse.visitor.common.memo.FieldMemo;
import util.sqlparse.visitor.common.memo.ValueMemo;
import util.sqlparse.visitor.common.scope.Scope;
import util.sqlparse.visitor.common.utils.Alias;

public class FieldVisitor extends ParseVisitor {
   private static final String QUESTION_MARK = "?";
   private static final String FUNC_IFNULL = "ifnull";
   private static final String FUNC_NVL = "nvl";
   private final Alias alias = new Alias();

   public FieldVisitor(Scope scope) {
      super(scope);
   }

   public boolean visit(SQLVariantRefExpr x) {
      this.setValue(x);
      return true;
   }

   private void setValue(SQLExpr valueExpr) {
      SQLObject parent = valueExpr.getParent();
      if (parent != null) {
         if (parent instanceof SQLUnaryExpr) {
            this.setUnary(valueExpr, parent);
         } else if (parent instanceof SQLBinaryOpExpr) {
            this.setBinary(valueExpr, parent);
         } else if (parent instanceof SQLCaseExpr.Item) {
            this.setCase(valueExpr, parent);
         } else if (parent instanceof SQLMethodInvokeExpr) {
            this.setMethod(valueExpr, parent);
         } else if (parent instanceof SQLExistsExpr) {
            this.setExists(valueExpr, parent);
         } else if (parent instanceof SQLInListExpr) {
            this.setInList(valueExpr, parent);
         } else if (parent instanceof SQLInsertStatement.ValuesClause) {
            this.setInsertOrReplaceItem(valueExpr, parent);
         } else if (parent instanceof SQLMergeStatement.MergeInsertClause) {
            this.setMergeInsertItem(valueExpr, parent);
         } else if (parent instanceof SQLMergeStatement.MergeUpdateClause) {
            this.setMergeUpdateItem(valueExpr, parent);
         } else if (parent instanceof SQLUpdateSetItem) {
            this.setUpdateItem(valueExpr, parent);
         } else if (parent instanceof SQLInSubQueryExpr) {
            this.setInSubQuery(valueExpr, parent);
         } else if (parent instanceof SQLLimit) {
            this.setLimit(valueExpr, parent);
         } else if (parent instanceof SQLBetweenExpr) {
            this.setBetween(valueExpr, parent);
         } else if (parent instanceof SQLSelectItem) {
            this.setSelect(valueExpr, (SQLSelectItem)parent);
         } else {
            this.setOther(valueExpr, parent);
         }
      }

   }

   private ValueMemo setSelect(SQLExpr valueExpr, SQLSelectItem parent) {
      SQLExpr expr = parent.getExpr();
      ValueMemo value = this.getValueMemo(expr, valueExpr);
      value.ref = valueExpr;
      value.expr = parent;
      value.value = this.getValue(valueExpr);
      value.isPreDefined = valueExpr.toString().equals("?");
      value.operator = "SELECT_ITEM";
      this.scope.getValues().add(value);
      return value;
   }

   private void setUnary(SQLExpr valueExpr, SQLObject parent) {
      ValueMemo value = this.getValueMemo((SQLExpr)null, valueExpr);
      value.ref = valueExpr;
      value.expr = parent;
      value.value = this.getValue(valueExpr);
      if (value.operator == null) {
         SQLUnaryExpr unary = (SQLUnaryExpr)parent;
         value.operator = unary.getOperator().name;
      } else {
         value.operator = value.operator.toUpperCase();
      }

      value.isPreDefined = value.toString().equals("?");
      this.scope.getValues().add(value);
   }

   private void setBinary(SQLExpr valueExpr, SQLObject parent) {
      SQLBinaryOpExpr opExpr = (SQLBinaryOpExpr)parent;
      SQLExpr leftExpr = opExpr.getLeft();
      SQLExpr rightExpr = opExpr.getRight();
      SQLExpr varExpr;
      if (leftExpr.equals(valueExpr)) {
         varExpr = rightExpr;
      } else {
         varExpr = leftExpr;
      }

      ValueMemo value = this.getValueMemo(varExpr, valueExpr);
      value.ref = valueExpr;
      value.expr = parent;
      value.value = this.getValue(valueExpr);
      value.operator = opExpr.getOperator().getName().toUpperCase();
      value.isPreDefined = value.toString().equals("?");
      this.scope.getValues().add(value);
   }

   private void setCase(SQLExpr valueExpr, SQLObject parent) {
      SQLCaseExpr.Item itemExpr = (SQLCaseExpr.Item)parent;
      SQLCaseExpr caseExpr = (SQLCaseExpr)itemExpr.getParent();
      ValueMemo value = this.getValueMemo(caseExpr.getValueExpr(), valueExpr);
      value.ref = valueExpr;
      value.expr = itemExpr;
      value.value = this.getValue(valueExpr);
      value.operator = "CASE";
      value.isPreDefined = value.toString().equals("?");
      this.scope.getValues().add(value);
   }

   private void setMethod(SQLExpr valueExpr, SQLObject parent) {
      if (!valueExpr.toString().equals("'%'")) {
         SQLMethodInvokeExpr methodExpr = (SQLMethodInvokeExpr)parent;
         String method = methodExpr.getMethodName();
         boolean done = false;
         if (method.equalsIgnoreCase("ifnull") || method.equalsIgnoreCase("nvl")) {
            SQLExpr expr = (SQLExpr)methodExpr.getArguments().get(0);
            ValueMemo value = this.getValueMemo(expr, valueExpr, true);
            if (value != null && value.field != null) {
               value.ref = valueExpr;
               value.expr = parent;
               value.value = this.getValue(valueExpr);
               value.operator = method.toUpperCase();
               value.isPreDefined = value.toString().equals("?");
               this.scope.getValues().add(value);
               done = true;
            }
         }

         if (!done) {
            List<String> operators = new ArrayList();
            SQLExpr expr = this.getSqlExpr(valueExpr.getParent(), valueExpr, operators);
            ValueMemo value = this.getValueMemo(expr, valueExpr);
            value.ref = valueExpr;
            value.expr = parent;
            value.value = this.getValue(valueExpr);
            if (value.operator == null) {
               if (operators.size() > 0) {
                  value.operator = (String)operators.get(0);
               } else {
                  value.operator = "OTHER";
               }
            }

            value.isPreDefined = value.toString().equals("?");
            this.scope.getValues().add(value);
         }

      }
   }

   private SQLExpr getSqlExpr(SQLObject x, SQLObject y, List<String> operators) {
      if (!(x instanceof SQLName) && !(x instanceof SQLValuableExpr)) {
         if (x instanceof SQLSelectItem) {
            return ((SQLSelectItem)x).getExpr();
         } else if (x == null) {
            return null;
         } else if (x instanceof SQLStatement) {
            return null;
         } else if (x instanceof SQLSelect) {
            return null;
         } else {
            if (x instanceof SQLBinaryOpExpr) {
               SQLBinaryOpExpr bin = (SQLBinaryOpExpr)x;
               if (bin.getOperator() != null) {
                  switch (bin.getOperator()) {
                     case Like:
                     case Like2:
                     case Like4:
                     case Likec:
                        SQLExpr cmp = bin.getLeft().equals(y) ? bin.getRight() : bin.getLeft();
                        operators.add("LIKE");
                        return cmp;
                     case Equality:
                     case GreaterThan:
                     case GreaterThanOrEqual:
                     case LessThan:
                     case LessThanOrEqual:
                        SQLExpr cmp2 = bin.getLeft().equals(y) ? bin.getRight() : bin.getLeft();
                        operators.add(bin.getOperator().getName().toUpperCase());
                        return cmp2;
                     case BooleanAnd:
                     case BooleanOr:
                        if (y instanceof SQLExpr) {
                           operators.add("OTHER");
                           return (SQLExpr)y;
                        }
                  }
               }
            }

            if (x instanceof SQLInListExpr) {
               operators.add("IN");
               SQLInListExpr inList = (SQLInListExpr)x;
               int idx = inList.getTargetList().indexOf(x);
               return idx != -1 ? (SQLExpr)inList.getTargetList().get(idx) : inList.getExpr();
            } else {
               SQLObject parent = x.getParent();
               return !(parent instanceof SQLExpr) ? null : this.getSqlExpr(parent, x, operators);
            }
         }
      } else {
         return (SQLExpr)x;
      }
   }

   private void setExists(SQLExpr valueExpr, SQLObject parent) {
      ValueMemo value = this.getValueMemo((SQLExpr)null, valueExpr);
      value.ref = valueExpr;
      value.expr = parent;
      value.value = this.getValue(valueExpr);
      value.isPreDefined = value.toString().equals("?");
      this.scope.getValues().add(value);
   }

   private void setInList(SQLExpr valueExpr, SQLObject parent) {
      SQLInListExpr inListExpr = (SQLInListExpr)parent;
      ValueMemo value = this.getValueMemo(inListExpr.getExpr(), valueExpr);
      value.ref = valueExpr;
      value.expr = inListExpr;
      value.value = this.getValue(valueExpr);
      value.isPreDefined = value.toString().equals("?");
      value.operator = "IN";
      this.scope.getValues().add(value);
   }

   private void setInsertOrReplaceItem(SQLExpr valueExpr, SQLObject parent) {
      SQLInsertStatement.ValuesClause clause = (SQLInsertStatement.ValuesClause)parent;
      int index = 0;

      for(int i = 0; i < clause.getValues().size(); ++i) {
         if (clause.getValues().get(i) == valueExpr) {
            index = i;
            break;
         }
      }

      List<FieldMemo> fields = null;
      List<SQLExpr> columns = null;
      if (clause.getParent() instanceof OracleInsertStatement) {
         OracleInsertStatement insert = (OracleInsertStatement)clause.getParent();
         columns = insert.getColumns();
         if (insert.getSelect() != null) {
            Scope selectScope = this.scope.getScope(insert.getSelect().getQuery());
            List<FieldMemo> valuesTemp = selectScope.getFieldMap().orderValues();

            for(int i = 0; i < valuesTemp.size(); ++i) {
               FieldMemo fieldMemo = (FieldMemo)valuesTemp.get(i);
               if (!fieldMemo.isSelectItem) {
                  valuesTemp.remove(fieldMemo);
               }
            }

            fields = valuesTemp;
         }
      } else if (clause.getParent() instanceof OracleMultiInsertStatement.InsertIntoClause) {
         OracleMultiInsertStatement.InsertIntoClause insert = (OracleMultiInsertStatement.InsertIntoClause)clause.getParent();
         columns = insert.getColumns();
         if (columns.size() == 0) {
            columns = clause.getValues();
         }
      } else if (clause.getParent() instanceof SQLReplaceStatement) {
         SQLReplaceStatement replace = (SQLReplaceStatement)clause.getParent();
         columns = replace.getColumns();
      }

      if (columns != null && columns.size() > 0) {
         int size = columns.size();
         index %= size;
         SQLExpr columnExpr = (SQLExpr)columns.get(index);
         ValueMemo value = this.getValueMemo(columnExpr, valueExpr);
         value.ref = valueExpr;
         value.expr = parent;
         value.value = this.getValue(valueExpr);
         value.isPreDefined = value.toString().equals("?");
         value.operator = "SET";
         this.scope.getValues().add(value);
      } else if (fields != null) {
         int size = fields.size();
         index %= size;
         FieldMemo field = (FieldMemo)fields.get(index);
         ValueMemo value = new ValueMemo();
         value.field = field;
         value.ref = valueExpr;
         value.expr = parent;
         value.value = this.getValue(valueExpr);
         value.isPreDefined = value.toString().equals("?");
         value.operator = "SET";
         this.scope.getValues().add(value);
      }

   }

   private void setMergeInsertItem(SQLExpr valueExpr, SQLObject parent) {
      SQLMergeStatement.MergeInsertClause clause = (SQLMergeStatement.MergeInsertClause)parent;
      int index = 0;

      for(int i = 0; i < clause.getValues().size(); ++i) {
         if (clause.getValues().get(i) == valueExpr) {
            index = i;
            break;
         }
      }

      List<SQLExpr> columns = clause.getColumns();
      if (columns != null && columns.size() > 0) {
         int size = columns.size();
         index %= size;
         SQLExpr columnExpr = (SQLExpr)columns.get(index);
         ValueMemo value = this.getValueMemo(columnExpr, valueExpr);
         value.ref = valueExpr;
         value.expr = parent;
         value.value = this.getValue(valueExpr);
         value.isPreDefined = value.toString().equals("?");
         value.operator = "SET";
         this.scope.getValues().add(value);
      }

   }

   private void setMergeUpdateItem(SQLExpr valueExpr, SQLObject parent) {
      SQLMergeStatement.MergeUpdateClause clause = (SQLMergeStatement.MergeUpdateClause)parent;
      SQLUpdateSetItem item = (SQLUpdateSetItem)clause.getItems().get(0);

      for(int i = 0; i < clause.getItems().size(); ++i) {
         if (((SQLUpdateSetItem)clause.getItems().get(i)).getValue() == valueExpr) {
            item = (SQLUpdateSetItem)clause.getItems().get(i);
            break;
         }
      }

      ValueMemo value = this.getValueMemo(item.getColumn(), valueExpr);
      value.ref = valueExpr;
      value.expr = parent;
      value.value = this.getValue(valueExpr);
      value.isPreDefined = value.toString().equals("?");
      value.operator = "SET";
      this.scope.getValues().add(value);
   }

   private void setUpdateItem(SQLExpr valueExpr, SQLObject parent) {
      SQLUpdateSetItem item = (SQLUpdateSetItem)parent;
      ValueMemo value = this.getValueMemo(item.getColumn(), valueExpr);
      value.ref = valueExpr;
      value.expr = parent;
      value.value = this.getValue(valueExpr);
      value.isPreDefined = value.toString().equals("?");
      value.operator = "SET";
      this.scope.getValues().add(value);
   }

   private void setInSubQuery(SQLExpr valueExpr, SQLObject parent) {
      ValueMemo value = this.getValueMemo((SQLExpr)null, valueExpr);
      value.ref = valueExpr;
      value.expr = parent;
      value.value = this.getValue(valueExpr);
      value.operator = "IN";
      this.scope.getValues().add(value);
   }

   private void setLimit(SQLExpr valueExpr, SQLObject parent) {
      SQLLimit limit = (SQLLimit)parent;
      if (limit.getOffset() != null && limit.getOffset() == valueExpr) {
         ValueMemo value = new ValueMemo();
         value.ref = limit.getOffset();
         value.expr = parent;
         value.value = this.getValue(valueExpr);
         value.isPreDefined = valueExpr.toString().equals("?");
         value.operator = "LIMIT";
         FieldMemo field = new FieldMemo();
         field.name = parent + "limit_offset";
         value.field = field;
         this.scope.getValues().add(value);
      }

      if (limit.getRowCount() != null && limit.getRowCount() == valueExpr) {
         ValueMemo value = new ValueMemo();
         value.ref = limit.getRowCount();
         value.expr = parent;
         value.value = this.getValue(valueExpr);
         value.isPreDefined = valueExpr.toString().equals("?");
         value.operator = "LIMIT";
         FieldMemo field = new FieldMemo();
         field.name = parent + "limit_rowcount";
         value.field = field;
         this.scope.getValues().add(value);
      }

   }

   private void setBetween(SQLExpr valueExpr, SQLObject parent) {
      SQLBetweenExpr between = (SQLBetweenExpr)parent;
      SQLExpr expr = valueExpr == between.getBeginExpr() ? between.getBeginExpr() : between.getEndExpr();
      ValueMemo value = this.getValueMemo(expr, valueExpr);
      value.ref = valueExpr;
      value.expr = parent;
      value.value = this.getValue(valueExpr);
      value.isPreDefined = valueExpr.toString().equals("?");
      value.operator = "BETWEEN";
      this.scope.getValues().add(value);
   }

   private ValueMemo getValueMemo(SQLExpr x, SQLExpr valueExpr) {
      return this.getValueMemo(x, valueExpr, false);
   }

   private ValueMemo getValueMemo(SQLExpr x, SQLExpr valueExpr, boolean nullIfNoField) {
      ValueMemo value = this.scope.getCurrent().getFieldValueInfo(x);
      if (value == null && !nullIfNoField) {
         value = new ValueMemo(valueExpr);
         FieldMemo field = new FieldMemo();
         field.name = this.alias.aliasField();
         value.field = field;
      }

      return value;
   }

   private void setOther(SQLExpr valueExpr, SQLObject parent) {
      ValueMemo value = this.getValueMemo((SQLExpr)null, valueExpr);
      value.ref = valueExpr;
      value.expr = parent;
      value.value = this.getValue(valueExpr);
      value.operator = "COMPLEX";
      this.scope.getValues().add(value);
   }

   private Object getValue(SQLExpr value) {
      if (value instanceof SQLNullExpr) {
         return "NULL";
      } else {
         return value instanceof SQLValuableExpr ? ((SQLValuableExpr)value).getValue() : value.toString();
      }
   }

   public boolean visit(SQLTimestampExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLNumberExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLDateExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLCharExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLNCharExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLIntegerExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLBigIntExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLBooleanExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLDateTimeExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLBinaryExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLDecimalExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLDoubleExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLFloatExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLHexExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLJSONExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLRealExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLSmallIntExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLTimeExpr x) {
      this.setValue(x);
      return true;
   }

   public boolean visit(SQLTinyIntExpr x) {
      this.setValue(x);
      return true;
   }
}
