package com.siimkinks.sqlitemagic;

import com.siimkinks.sqlitemagic.internal.SimpleArrayMap;
import com.siimkinks.sqlitemagic.internal.StringArraySet;

import java.util.ArrayList;
import java.util.LinkedList;

import androidx.annotation.CheckResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Size;

/**
 * Metadata of a table in a database.
 *
 * @param <T> Table Java object type
 */
public class Table<T> {
  public static final Table<?> ANONYMOUS_TABLE = new Table<>("", null, 1);

  @NonNull
  final String name;
  @Nullable
  final String alias;
  @NonNull
  final String nameInQuery;
  final int nrOfColumns;
  final boolean hasAlias;
  private final Column<?, ?, ?, T, NotNullable> selectAllColumn;

  Table(@NonNull String name, @Nullable String alias, int nrOfColumns) {
    this.name = name;
    this.alias = alias;
    this.nrOfColumns = nrOfColumns;
    final boolean hasAlias = alias != null;
    this.hasAlias = hasAlias;
    this.nameInQuery = hasAlias ? alias : name;
    this.selectAllColumn = new Column<>(this, "*", true, null, false, null);
  }

  void appendToSqlFromClause(@NonNull StringBuilder sb) {
    sb.append(name);
    if (hasAlias) {
      sb.append(" AS ")
          .append(alias);
    }
  }

  /**
   * This method gives table class the opportunity to perfect selection at select statement
   * build time.
   *
   * @param observedTables      Tables that are being selected
   * @param tableGraphNodeNames Selection graph node names
   * @param columnPositions     Column positions in the selection
   * @return Whether selection should be deep.
   */
  boolean perfectSelection(@NonNull ArrayList<String> observedTables,
                           @Nullable SimpleArrayMap<String, String> tableGraphNodeNames,
                           @Nullable SimpleArrayMap<String, Integer> columnPositions) {
    if (!observedTables.contains(name)) {
      observedTables.add(name);
    }
    return false;
  }

  @NonNull
  final Table<T> internalAlias(@NonNull String alias) {
    return new Table<>(name, alias, nrOfColumns);
  }

  /**
   * Create an alias for this table.
   *
   * @param alias The alias name
   * @return New table with provided alias.
   */
  @NonNull
  @CheckResult
  public Table<T> as(@NonNull String alias) {
    return new Table<>(name, alias, nrOfColumns);
  }

  /**
   * All columns from this table ("*").
   *
   * @return Column that represents all columns in this table
   */
  @NonNull
  public final Column<?, ?, ?, T, NotNullable> all() {
    return selectAllColumn;
  }

  /**
   * Create join "ON" clause.
   *
   * @param expr Expression to use in join "ON" clause
   * @return Join clause
   */
  @NonNull
  @CheckResult
  public final JoinClause on(@NonNull final Expr expr) {
    return new JoinClause(this, "", "ON ") {
      @Override
      void appendSql(@NonNull StringBuilder sb) {
        super.appendSql(sb);
        expr.appendToSql(sb);
      }

      @Override
      void appendSql(@NonNull StringBuilder sb, @NonNull SimpleArrayMap<String, LinkedList<String>> systemRenamedTables) {
        super.appendSql(sb, systemRenamedTables);
        expr.appendToSql(sb, systemRenamedTables);
      }

      @Override
      boolean containsColumn(@NonNull Column<?, ?, ?, ?, ?> column) {
        return expr.containsColumn(column);
      }

      @Override
      void addArgs(@NonNull ArrayList<String> args) {
        super.addArgs(args);
        expr.addArgs(args);
      }
    };
  }

  /**
   * Create join "USING" clause.
   * <p>
   * Each of the columns specified must exist in the datasets to both the left and right
   * of the join-operator. For each pair of columns, the expression "lhs.X = rhs.X"
   * is evaluated for each row of the cartesian product as a boolean expression.
   * Only rows for which all such expressions evaluates to true are included from the
   * result set. When comparing values as a result of a USING clause, the normal rules
   * for handling affinities, collation sequences and NULL values in comparisons apply.
   * The column from the dataset on the left-hand side of the join-operator is considered
   * to be on the left-hand side of the comparison operator (=) for the purposes of
   * collation sequence and affinity precedence.
   * <p>
   * For each pair of columns identified by a USING clause, the column from the
   * right-hand dataset is omitted from the joined dataset. This is the only difference
   * between a USING clause and its equivalent ON constraint.
   *
   * @param columns Columns to use in the USING clause
   * @return Join clause
   */
  @NonNull
  @CheckResult
  public final JoinClause using(@NonNull @Size(min = 1) final Column... columns) {
    final int colLen = columns.length;
    final StringBuilder sb = new StringBuilder(8 + colLen * 12);
    sb.append("USING (");
    for (int i = 0; i < colLen; i++) {
      if (i > 0) {
        sb.append(',');
      }
      sb.append(columns[i].name);
    }
    sb.append(')');
    return new JoinClause(this, "", sb.toString()) {
      @Override
      boolean containsColumn(@NonNull Column<?, ?, ?, ?, ?> column) {
        for (int i = 0; i < colLen; i++) {
          if (columns[i].equals(column)) {
            return true;
          }
        }
        return false;
      }
    };
  }

  @Nullable
  SimpleArrayMap<String, LinkedList<String>> addDeepQueryParts(@NonNull Select.From from,
                                                               @Nullable StringArraySet selectFromTables,
                                                               @Nullable SimpleArrayMap<String, String> tableGraphNodeNames,
                                                               boolean select1) {
    return null;
  }

  @Nullable
  SimpleArrayMap<String, LinkedList<String>> addShallowQueryParts(@NonNull Select.From from,
                                                                  @Nullable StringArraySet selectFromTables,
                                                                  @Nullable SimpleArrayMap<String, String> tableGraphNodeNames,
                                                                  boolean select1) {
    return null;
  }

  @NonNull
  Query.Mapper<T> mapper(@Nullable final SimpleArrayMap<String, Integer> columnPositions,
                         SimpleArrayMap<String, String> tableGraphNodeNames,
                         final boolean queryDeep) {
    throw new RuntimeException("not implemented");
  }

  final boolean baseNameEquals(Object o) {
    if (this == o) return true;
    if (o == null) return false;

    final Table<?> table;
    try {
      table = (Table<?>) o;
    } catch (ClassCastException e) {
      return false;
    }

    return name.equals(table.name);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null) return false;

    final Table<?> table;
    try {
      table = (Table<?>) o;
    } catch (ClassCastException e) {
      return false;
    }

    return nameInQuery.equals(table.nameInQuery);
  }

  @Override
  public int hashCode() {
    return nameInQuery.hashCode();
  }

  @Override
  public String toString() {
    return name;
  }
}