package com.chenyang.druid.sql.ast.statement;

import com.chenyang.druid.FastsqlColumnAmbiguousException;
import com.chenyang.druid.FastsqlException;
import com.chenyang.druid.sql.SQLUtils;
import com.chenyang.druid.sql.ast.SQLExpr;
import com.chenyang.druid.sql.ast.SQLName;
import com.chenyang.druid.sql.ast.SQLObject;
import com.chenyang.druid.sql.ast.SQLObjectImpl;
import com.chenyang.druid.sql.ast.SQLReplaceable;
import com.chenyang.druid.sql.ast.expr.SQLBinaryOpExpr;
import com.chenyang.druid.sql.ast.expr.SQLIdentifierExpr;
import com.chenyang.druid.sql.ast.expr.SQLPropertyExpr;
import com.chenyang.druid.sql.repository.SchemaResolveVisitor;
import com.chenyang.druid.sql.visitor.SQLASTVisitor;
import com.chenyang.druid.util.FnvHash;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class SQLJoinTableSource extends SQLTableSourceImpl implements SQLReplaceable {
   protected SQLTableSource left;
   protected JoinType joinType;
   protected SQLTableSource right;
   protected SQLExpr condition;
   protected final List<SQLExpr> using = new ArrayList();
   protected boolean natural = false;
   protected UDJ udj;
   protected boolean asof;

   public SQLJoinTableSource(String alias) {
      super(alias);
   }

   public SQLJoinTableSource() {
   }

   public SQLJoinTableSource(SQLTableSource left, JoinType joinType, SQLTableSource right, SQLExpr condition) {
      this.setLeft(left);
      this.setJoinType(joinType);
      this.setRight(right);
      this.setCondition(condition);
   }

   protected void accept0(SQLASTVisitor visitor) {
      if (visitor.visit(this)) {
         if (this.left != null) {
            this.left.accept(visitor);
         }

         if (this.right != null) {
            this.right.accept(visitor);
         }

         if (this.condition != null) {
            this.condition.accept(visitor);
         }

         for(int i = 0; i < this.using.size(); ++i) {
            SQLExpr item = (SQLExpr)this.using.get(i);
            if (item != null) {
               item.accept(visitor);
            }
         }

         if (this.udj != null) {
            this.udj.accept(visitor);
         }
      }

      visitor.endVisit(this);
   }

   public UDJ getUdj() {
      return this.udj;
   }

   public void setUdj(UDJ x) {
      if (x != null) {
         x.setParent(this);
      }

      this.udj = x;
   }

   public boolean isAsof() {
      return this.asof;
   }

   public void setAsof(boolean asof) {
      this.asof = asof;
   }

   public JoinType getJoinType() {
      return this.joinType;
   }

   public void setJoinType(JoinType joinType) {
      this.joinType = joinType;
   }

   public void setImplicitJoinToCross() {
      if (this.joinType == JoinType.COMMA) {
         this.joinType = JoinType.CROSS_JOIN;
      }

      if (this.left instanceof SQLJoinTableSource) {
         ((SQLJoinTableSource)this.left).setImplicitJoinToCross();
      }

      if (this.right instanceof SQLJoinTableSource) {
         ((SQLJoinTableSource)this.right).setImplicitJoinToCross();
      }

   }

   public SQLTableSource getLeft() {
      return this.left;
   }

   public void setLeft(SQLTableSource left) {
      if (left != null) {
         left.setParent(this);
      }

      this.left = left;
   }

   public void setLeft(String tableName, String alias) {
      SQLExprTableSource tableSource;
      if (tableName != null && tableName.length() != 0) {
         tableSource = new SQLExprTableSource(new SQLIdentifierExpr(tableName), alias);
      } else {
         tableSource = null;
      }

      this.setLeft(tableSource);
   }

   public void setRight(String tableName, String alias) {
      SQLExprTableSource tableSource;
      if (tableName != null && tableName.length() != 0) {
         tableSource = new SQLExprTableSource(new SQLIdentifierExpr(tableName), alias);
      } else {
         tableSource = null;
      }

      this.setRight(tableSource);
   }

   public SQLTableSource getRight() {
      return this.right;
   }

   public void setRight(SQLTableSource right) {
      if (right != null) {
         right.setParent(this);
      }

      this.right = right;
   }

   public SQLExpr getCondition() {
      return this.condition;
   }

   public void setCondition(SQLExpr condition) {
      if (condition != null) {
         condition.setParent(this);
      }

      this.condition = condition;
   }

   public void addCondition(SQLExpr condition) {
      if (this.condition == null) {
         this.condition = condition;
         this.setImplicitJoinToCross();
      } else {
         this.condition = SQLBinaryOpExpr.and(this.condition, condition);
      }
   }

   public void addConditionnIfAbsent(SQLExpr condition) {
      if (!this.containsCondition(condition)) {
         this.condition = SQLBinaryOpExpr.and(this.condition, condition);
      }
   }

   public boolean containsCondition(SQLExpr condition) {
      if (this.condition == null) {
         return false;
      } else if (this.condition.equals(condition)) {
         return false;
      } else {
         return this.condition instanceof SQLBinaryOpExpr ? ((SQLBinaryOpExpr)this.condition).contains(condition) : false;
      }
   }

   public List<SQLExpr> getUsing() {
      return this.using;
   }

   public boolean isNatural() {
      return this.natural;
   }

   public void setNatural(boolean natural) {
      this.natural = natural;
   }

   public void output(Appendable buf) {
      try {
         this.left.output(buf);
         buf.append(' ');
         buf.append(JoinType.toString(this.joinType));
         buf.append(' ');
         this.right.output(buf);
         if (this.condition != null) {
            buf.append(" ON ");
            this.condition.output(buf);
         }

      } catch (IOException ex) {
         throw new FastsqlException("output error", ex);
      }
   }

   public boolean replace(SQLExpr expr, SQLExpr target) {
      if (this.condition == expr) {
         this.setCondition(target);
         return true;
      } else {
         for(int i = 0; i < this.using.size(); ++i) {
            if (this.using.get(i) == expr) {
               target.setParent(this);
               this.using.set(i, target);
               return true;
            }
         }

         return false;
      }
   }

   public boolean replace(SQLTableSource cmp, SQLTableSource target) {
      if (this.left == cmp) {
         if (target == null) {
            SQLUtils.replaceInParent((SQLTableSource)this, (SQLTableSource)this.right);
         } else {
            this.setLeft(target);
         }

         return true;
      } else if (this.right == cmp) {
         if (target == null) {
            SQLUtils.replaceInParent((SQLTableSource)this, (SQLTableSource)this.left);
         } else {
            this.setRight(target);
         }

         return true;
      } else {
         return false;
      }
   }

   public void cloneTo(SQLJoinTableSource x) {
      x.alias = this.alias;
      if (this.left != null) {
         x.setLeft(this.left.clone());
      }

      x.joinType = this.joinType;
      if (this.right != null) {
         x.setRight(this.right.clone());
      }

      if (this.condition != null) {
         x.setCondition(this.condition.clone());
      }

      for(SQLExpr item : this.using) {
         SQLExpr item2 = item.clone();
         item2.setParent(x);
         x.using.add(item2);
      }

      x.natural = this.natural;
      x.asof = this.asof;
      if (this.udj != null) {
         x.udj = this.udj.clone();
      }

   }

   public SQLJoinTableSource clone() {
      SQLJoinTableSource x = new SQLJoinTableSource();
      this.cloneTo(x);
      return x;
   }

   public void reverse() {
      SQLTableSource temp = this.left;
      this.left = this.right;
      this.right = temp;
      if (this.left instanceof SQLJoinTableSource) {
         ((SQLJoinTableSource)this.left).reverse();
      }

      if (this.right instanceof SQLJoinTableSource) {
         ((SQLJoinTableSource)this.right).reverse();
      }

   }

   public void rearrangement() {
      if (this.joinType == JoinType.COMMA || this.joinType == JoinType.INNER_JOIN) {
         if (this.right instanceof SQLJoinTableSource) {
            SQLJoinTableSource rightJoin = (SQLJoinTableSource)this.right;
            if (rightJoin.joinType != JoinType.COMMA && rightJoin.joinType != JoinType.INNER_JOIN) {
               return;
            }

            SQLTableSource a = this.left;
            SQLTableSource b = rightJoin.getLeft();
            SQLTableSource c = rightJoin.getRight();
            SQLExpr on_ab = this.condition;
            SQLExpr on_bc = rightJoin.condition;
            this.setLeft(rightJoin);
            rightJoin.setLeft(a);
            rightJoin.setRight(b);
            boolean on_ab_match = false;
            if (on_ab instanceof SQLBinaryOpExpr) {
               SQLBinaryOpExpr on_ab_binaryOpExpr = (SQLBinaryOpExpr)on_ab;
               if (on_ab_binaryOpExpr.getLeft() instanceof SQLPropertyExpr && on_ab_binaryOpExpr.getRight() instanceof SQLPropertyExpr) {
                  String leftOwnerName = ((SQLPropertyExpr)on_ab_binaryOpExpr.getLeft()).getOwnernName();
                  String rightOwnerName = ((SQLPropertyExpr)on_ab_binaryOpExpr.getRight()).getOwnernName();
                  if (rightJoin.containsAlias(leftOwnerName) && rightJoin.containsAlias(rightOwnerName)) {
                     on_ab_match = true;
                  }
               }
            }

            if (on_ab_match) {
               rightJoin.setCondition(on_ab);
            } else {
               rightJoin.setCondition((SQLExpr)null);
               on_bc = SQLBinaryOpExpr.and(on_bc, on_ab);
            }

            this.setRight(c);
            this.setCondition(on_bc);
         }

      }
   }

   public boolean contains(SQLTableSource tableSource, SQLExpr condition) {
      if (this.right.equals(tableSource)) {
         if (this.condition == condition) {
            return true;
         } else {
            return this.condition != null && this.condition.equals(condition);
         }
      } else if (this.left instanceof SQLJoinTableSource) {
         SQLJoinTableSource joinLeft = (SQLJoinTableSource)this.left;
         if (tableSource instanceof SQLJoinTableSource) {
            SQLJoinTableSource join = (SQLJoinTableSource)tableSource;
            if (join.right.equals(this.right) && this.condition.equals(condition) && joinLeft.right.equals(join.left)) {
               return true;
            }
         }

         return joinLeft.contains(tableSource, condition);
      } else {
         return false;
      }
   }

   public boolean contains(SQLTableSource tableSource, SQLExpr condition, JoinType joinType) {
      if (this.right.equals(tableSource)) {
         if (this.condition == condition) {
            return true;
         } else {
            return this.condition != null && this.condition.equals(condition) && this.joinType == joinType;
         }
      } else if (this.left instanceof SQLJoinTableSource) {
         SQLJoinTableSource joinLeft = (SQLJoinTableSource)this.left;
         if (tableSource instanceof SQLJoinTableSource) {
            SQLJoinTableSource join = (SQLJoinTableSource)tableSource;
            if (join.right.equals(this.right) && this.condition != null && this.condition.equals(join.condition) && joinLeft.right.equals(join.left) && this.joinType == join.joinType && joinLeft.condition != null && joinLeft.condition.equals(condition) && joinLeft.joinType == joinType) {
               return true;
            }
         }

         return joinLeft.contains(tableSource, condition, joinType);
      } else {
         return false;
      }
   }

   public SQLJoinTableSource findJoin(SQLTableSource tableSource, JoinType joinType) {
      if (this.right.equals(tableSource)) {
         return this.joinType == joinType ? this : null;
      } else {
         return this.left instanceof SQLJoinTableSource ? ((SQLJoinTableSource)this.left).findJoin(tableSource, joinType) : null;
      }
   }

   public boolean containsAlias(String alias) {
      if (SQLUtils.nameEquals(this.alias, alias)) {
         return true;
      } else if (this.left != null && this.left.containsAlias(alias)) {
         return true;
      } else {
         return this.right != null && this.right.containsAlias(alias);
      }
   }

   public SQLColumnDefinition findColumn(String columnName) {
      long hash = FnvHash.hashCode64(columnName);
      return this.findColumn(hash);
   }

   public SQLColumnDefinition findColumn(long columnNameHash) {
      SQLObject column = this.resolveColum(columnNameHash);
      return column instanceof SQLColumnDefinition ? (SQLColumnDefinition)column : null;
   }

   public SQLObject resolveColum(long columnNameHash) {
      if (this.left != null) {
         SQLObject column = this.left.resolveColum(columnNameHash);
         if (column != null) {
            return column;
         }
      }

      return this.right != null ? this.right.resolveColum(columnNameHash) : null;
   }

   public SQLTableSource findTableSourceWithColumn(String columnName) {
      long hash = FnvHash.hashCode64(columnName);
      return this.findTableSourceWithColumn(hash, columnName, 0);
   }

   public SQLJoinTableSource findTableSourceWithColumn(SQLName a, SQLName b) {
      if (this.left.findTableSourceWithColumn(a) != null && this.right.findTableSourceWithColumn(b) != null) {
         return this;
      } else if (this.right.findTableSourceWithColumn(a) != null && this.left.findTableSourceWithColumn(b) != null) {
         return this;
      } else if (this.left instanceof SQLJoinTableSource) {
         return ((SQLJoinTableSource)this.left).findTableSourceWithColumn(a, b);
      } else {
         return this.right instanceof SQLJoinTableSource ? ((SQLJoinTableSource)this.right).findTableSourceWithColumn(a, b) : null;
      }
   }

   public SQLTableSource findTableSourceWithColumn(long columnNameHash, String name, int option) {
      SQLTableSource leftMatch = null;
      if (this.left != null) {
         leftMatch = this.left.findTableSourceWithColumn(columnNameHash, name, option);
      }

      boolean checkColumnAmbiguous = (option & SchemaResolveVisitor.Option.CheckColumnAmbiguous.mask) != 0;
      if (leftMatch != null && !checkColumnAmbiguous) {
         return leftMatch;
      } else {
         SQLTableSource rightMatch = null;
         if (this.right != null) {
            rightMatch = this.right.findTableSourceWithColumn(columnNameHash, name, option);
         }

         if (leftMatch == null) {
            return rightMatch;
         } else if (rightMatch == null) {
            return leftMatch;
         } else if (name != null) {
            String msg = "Column '" + name + "' is ambiguous";
            throw new FastsqlColumnAmbiguousException(msg);
         } else {
            throw new FastsqlColumnAmbiguousException();
         }
      }
   }

   public boolean match(String alias_a, String alias_b) {
      if (this.left != null && this.right != null) {
         if (this.left.containsAlias(alias_a) && this.right.containsAlias(alias_b)) {
            return true;
         } else {
            return this.right.containsAlias(alias_a) && this.left.containsAlias(alias_b);
         }
      } else {
         return false;
      }
   }

   public boolean conditionContainsTable(String alias) {
      if (this.condition == null) {
         return false;
      } else {
         return this.condition instanceof SQLBinaryOpExpr ? ((SQLBinaryOpExpr)this.condition).conditionContainsTable(alias) : false;
      }
   }

   public SQLJoinTableSource join(SQLTableSource right, JoinType joinType, SQLExpr condition) {
      SQLJoinTableSource joined = new SQLJoinTableSource(this, joinType, right, condition);
      return joined;
   }

   public SQLTableSource findTableSource(long alias_hash) {
      if (alias_hash == 0L) {
         return null;
      } else if (this.aliasHashCode64() == alias_hash) {
         return this;
      } else {
         SQLTableSource result = this.left.findTableSource(alias_hash);
         return result != null ? result : this.right.findTableSource(alias_hash);
      }
   }

   public SQLTableSource other(SQLTableSource x) {
      if (this.left == x) {
         return this.right;
      } else {
         return this.right == x ? this.left : null;
      }
   }

   public int hashCode() {
      int result = this.left != null ? this.left.hashCode() : 0;
      result = 31 * result + (this.joinType != null ? this.joinType.hashCode() : 0);
      result = 31 * result + (this.right != null ? this.right.hashCode() : 0);
      result = 31 * result + (this.condition != null ? this.condition.hashCode() : 0);
      result = 31 * result + this.using.hashCode();
      result = 31 * result + (this.natural ? 1 : 0);
      return result;
   }

   public boolean equals(Object o) {
      if (this == o) {
         return true;
      } else if (o != null && this.getClass() == o.getClass()) {
         SQLJoinTableSource that = (SQLJoinTableSource)o;
         if (this.natural != that.natural) {
            return false;
         } else {
            if (this.left != null) {
               if (!this.left.equals(that.left)) {
                  return false;
               }
            } else if (that.left != null) {
               return false;
            }

            if (this.joinType != that.joinType) {
               return false;
            } else {
               if (this.right != null) {
                  if (!this.right.equals(that.right)) {
                     return false;
                  }
               } else if (that.right != null) {
                  return false;
               }

               if (this.condition != null) {
                  if (!this.condition.equals(that.condition)) {
                     return false;
                  }
               } else if (that.condition != null) {
                  return false;
               }

               return this.using.equals(that.using);
            }
         }
      } else {
         return false;
      }
   }

   public void splitTo(List<SQLTableSource> outTableSources, JoinType joinType) {
      if (joinType == this.joinType) {
         if (this.left instanceof SQLJoinTableSource) {
            ((SQLJoinTableSource)this.left).splitTo(outTableSources, joinType);
         } else {
            outTableSources.add(this.left);
         }

         if (this.right instanceof SQLJoinTableSource) {
            ((SQLJoinTableSource)this.right).splitTo(outTableSources, joinType);
         } else {
            outTableSources.add(this.right);
         }
      } else {
         outTableSources.add(this);
      }

   }

   public static enum JoinType {
      COMMA(","),
      JOIN("JOIN"),
      INNER_JOIN("INNER JOIN"),
      FULL_JOIN("FULL JOIN"),
      CROSS_JOIN("CROSS JOIN"),
      NATURAL_JOIN("NATURAL JOIN"),
      NATURAL_CROSS_JOIN("NATURAL CROSS JOIN"),
      NATURAL_OUTER_JOIN("NATURAL OUTER JOIN"),
      NATURAL_LEFT_JOIN("NATURAL LEFT JOIN"),
      NATURAL_RIGHT_JOIN("NATURAL RIGHT JOIN"),
      NATURAL_INNER_JOIN("NATURAL INNER JOIN"),
      LEFT_OUTER_JOIN("LEFT OUTER JOIN"),
      LEFT_SEMI_JOIN("LEFT SEMI JOIN"),
      LEFT_ANTI_JOIN("LEFT ANTI JOIN"),
      RIGHT_OUTER_JOIN("RIGHT OUTER JOIN"),
      FULL_OUTER_JOIN("FULL OUTER JOIN"),
      STRAIGHT_JOIN("STRAIGHT_JOIN"),
      OUTER_APPLY("OUTER APPLY"),
      CROSS_APPLY("CROSS APPLY"),
      NATURAL_FULL_OUTER_JOIN("NATURAL FULL OUTER JOIN"),
      NATURAL_FULL_JOIN("NATURAL FULL JOIN"),
      NATURAL_LEFT_OUTER_JOIN("NATURAL LEFT OUTER JOIN"),
      NATURAL_LEFT_SEMI_JOIN("NATURAL LEFT SEMI JOIN"),
      NATURAL_LEFT_ANTI_JOIN("NATURAL LEFT ANTI JOIN"),
      NATURAL_RIGHT_OUTER_JOIN("NATURAL RIGHT OUTER JOIN"),
      NATURAL_RIGHT_SEMI_JOIN("NATURAL RIGHT SEMI JOIN"),
      NATURAL_RIGHT_ANTI_JOIN("NATURAL RIGHT ANTI JOIN");

      public final String name;
      public final String name_lcase;

      private JoinType(String name) {
         this.name = name;
         this.name_lcase = name.toLowerCase();
      }

      public static String toString(JoinType joinType) {
         return joinType.name;
      }
   }

   public static class UDJ extends SQLObjectImpl {
      protected String function;
      protected final List<SQLExpr> arguments = new ArrayList();
      protected String alias;
      protected final List<SQLName> columns = new ArrayList();

      public UDJ() {
      }

      protected void accept0(SQLASTVisitor v) {
         if (v.visit(this)) {
            this.acceptChild(v, this.arguments);
            this.acceptChild(v, this.columns);
         }

         v.endVisit(this);
      }

      public UDJ clone() {
         UDJ x = new UDJ();
         x.function = this.function;

         for(SQLExpr arg : this.arguments) {
            SQLExpr t = arg.clone();
            t.setParent(x);
            x.arguments.add(t);
         }

         x.alias = this.alias;

         for(SQLName column : this.columns) {
            SQLName t = column.clone();
            t.setParent(x);
            x.columns.add(t);
         }

         return x;
      }

      public UDJ(String function) {
         this.function = function;
      }

      public String getFunction() {
         return this.function;
      }

      public void setFunction(String function) {
         this.function = function;
      }

      public List<SQLExpr> getArguments() {
         return this.arguments;
      }

      public List<SQLName> getColumns() {
         return this.columns;
      }

      public String getAlias() {
         return this.alias;
      }

      public void setAlias(String alias) {
         this.alias = alias;
      }
   }
}
