package com.avaje.ebean.dbmigration.model.build;

import com.avaje.ebean.dbmigration.ddlgeneration.platform.util.IndexSet;
import com.avaje.ebean.dbmigration.model.MColumn;
import com.avaje.ebean.dbmigration.model.MCompoundForeignKey;
import com.avaje.ebean.dbmigration.model.MTable;
import com.avaje.ebean.dbmigration.model.visitor.BaseTablePropertyVisitor;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocOne;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyCompound;
import com.avaje.ebeaninternal.server.deploy.IndexDefinition;
import com.avaje.ebeaninternal.server.deploy.InheritInfo;
import com.avaje.ebeaninternal.server.deploy.TableJoinColumn;
import com.avaje.ebeaninternal.server.deploy.id.ImportedId;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * Used as part of ModelBuildBeanVisitor and generally adds the MColumn to the associated
 * MTable model objects.
 */
public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor {

  protected final ModelBuildContext ctx;

  private final MTable table;

  private final BeanDescriptor<?> beanDescriptor;

  private final IndexSet indexSet = new IndexSet();

  private MColumn lastColumn;

  private int countForeignKey;
  private int countIndex;
  private int countUnique;
  private int countCheck;


  public ModelBuildPropertyVisitor(ModelBuildContext ctx, MTable table, BeanDescriptor<?> beanDescriptor) {
    this.ctx = ctx;
    this.table = table;
    this.beanDescriptor = beanDescriptor;
    addIndexes(beanDescriptor.getIndexDefinitions());
  }

  /**
   * Add unique constraints defined via JPA UniqueConstraint annotations.
   */
  private void addIndexes(IndexDefinition[] indexes) {

    if (indexes != null) {
      for (int i = 0; i < indexes.length; i++) {
        IndexDefinition index = indexes[i];
        String[] columns = index.getColumns();
        indexSet.add(columns);

        if (index.isUnique()) {
          String uqName = index.getName();
          if (uqName == null || uqName.trim().isEmpty()) {
            uqName = determineUniqueConstraintName(columns);
          }
          table.addUniqueConstraint(columns, false, uqName);

        } else {
          // 'just' an index (not a unique constraint)
          String idxName = index.getName();
          if (idxName == null || idxName.trim().isEmpty()) {
            idxName = determineIndexName(columns);
          }
          ctx.addIndex(idxName, table.getName(), columns);
        }
      }
    }
  }

  @Override
  public void visitEnd() {

    // set the primary key name
    table.setPkName(determinePrimaryKeyName());

    // check if indexes on foreign keys should be suppressed
    for (MColumn column : table.allColumns()) {
      if (hasValue(column.getForeignKeyIndex())) {
        if (indexSet.contains(column.getName())) {
          // suppress index on foreign key as there is already
          // effectively an index (probably via unique constraint)
          column.setForeignKeyIndex(null);
        }
      }
    }

    for (MCompoundForeignKey compoundKey : table.getCompoundKeys()) {
      if (indexSet.contains(compoundKey.getColumns())) {
        // suppress index on foreign key as there is already
        // effectively an index (probably via unique constraint)
        compoundKey.setIndexName(null);
      }
    }

    addDraftTable();
  }

  /**
   * Create a 'draft' table that is mostly the same as the base table.
   * It has @DraftOnly columns and adjusted primary and foreign keys.
   */
  private void addDraftTable() {
    if (beanDescriptor.isDraftable() || beanDescriptor.isDraftableElement()) {
      // create a 'Draft' table which looks very similar (change PK, FK etc)
      ctx.createDraft(table, !beanDescriptor.isDraftableElement());
    }
  }


  @Override
  public void visitMany(BeanPropertyAssocMany<?> p) {
    if (p.isManyToMany()) {
      if (p.getMappedBy() == null) {
        // only create on other 'owning' side

        //TableJoin intersectionTableJoin = p.getIntersectionTableJoin();
        // check if the intersection table has already been created

        // build the create table and fkey constraints
        // putting the DDL into ctx for later output as we are
        // in the middle of rendering the create table DDL
        new ModelBuildIntersectionTable(ctx, p).build();
      }
    }
  }

  @Override
  public void visitCompoundScalar(BeanPropertyCompound compound, BeanProperty p) {
    visitScalar(p);
  }

  @Override
  public void visitCompound(BeanPropertyCompound p) {
    // do nothing
  }

  @Override
  public void visitEmbeddedScalar(BeanProperty p, BeanPropertyAssocOne<?> embedded) {

    visitScalar(p);
    if (embedded.isId()) {
      // compound primary key
      lastColumn.setPrimaryKey(true);
    }
  }

  @Override
  public void visitOneImported(BeanPropertyAssocOne<?> p) {

    TableJoinColumn[] columns = p.getTableJoin().columns();
    if (columns.length == 0) {
      String msg = "No join columns for " + p.getFullBeanName();
      throw new RuntimeException(msg);
    }

    ImportedId importedId = p.getImportedId();

    List<MColumn> modelColumns = new ArrayList<MColumn>(columns.length);

    MCompoundForeignKey compoundKey = null;
    if (columns.length > 1) {
      // compound foreign key
      String refTable = p.getTargetDescriptor().getBaseTable();
      String fkName = determineForeignKeyConstraintName(p.getName());
      String fkIndex = determineForeignKeyIndexName(p.getName());
      compoundKey = new MCompoundForeignKey(fkName, refTable, fkIndex);
      table.addForeignKey(compoundKey);
    }

    for (int i = 0; i < columns.length; i++) {

      String dbCol = columns[i].getLocalDbColumn();
      BeanProperty importedProperty = importedId.findMatchImport(dbCol);
      if (importedProperty == null) {
        throw new RuntimeException("Imported BeanProperty not found?");
      }
      String columnDefn = ctx.getColumnDefn(importedProperty, true);
      String refColumn = importedProperty.getDbColumn();

      MColumn col = table.addColumn(dbCol, columnDefn, !p.isNullable());

      if (columns.length == 1) {
        // single references column (put it on the column)
        String refTable = importedProperty.getBeanDescriptor().getBaseTable();
        if (refTable == null) {
          // odd case where an EmbeddedId only has 1 property
          refTable = p.getTargetDescriptor().getBaseTable();
        }
        col.setReferences(refTable + "." + refColumn);
        col.setForeignKeyName(determineForeignKeyConstraintName(col.getName()));
        col.setForeignKeyIndex(determineForeignKeyIndexName(col.getName()));
      } else {
        compoundKey.addColumnPair(dbCol, refColumn);
      }
      modelColumns.add(col);
    }

    if (p.isOneToOne()) {
      // adding the unique constraint restricts the cardinality from OneToMany down to OneToOne
      // for MsSqlServer we need different DDL to handle NULL values on this constraint
      if (modelColumns.size() == 1) {
        MColumn col = modelColumns.get(0);
        col.setUniqueOneToOne(determineUniqueConstraintName(col.getName()));
        indexSetAdd(col.getName());

      } else {
        String uqName = determineUniqueConstraintName(p.getName());
        table.addUniqueConstraint(modelColumns, true, uqName);
        indexSetAdd(modelColumns);
      }
    }
  }

  @Override
	public void visitScalar(BeanProperty p) {

    if (p.isSecondaryTable()) {
      lastColumn = null;
      return;
    }

    // using non-strict mode to render the DB type such that we have a
    // "logical" type like jsonb(200) that can map to JSONB or VARCHAR(200)
    MColumn col = new MColumn(p.getDbColumn(), ctx.getColumnDefn(p, false));
    col.setDefaultValue(p.getDbColumnDefault());
    col.setComment(p.getDbComment());
    col.setDraftOnly(p.isDraftOnly());

    if (p.isId()) {
      col.setPrimaryKey(true);
      if (p.getBeanDescriptor().isUseIdGenerator()) {
        col.setIdentity(true);
      }
    } else if (!p.isNullable() || p.isDDLNotNull()) {
      col.setNotnull(true);
    }

    if (p.isUnique() && !p.isId()) {
      col.setUnique(determineUniqueConstraintName(col.getName()));
      indexSetAdd(col.getName());
    }
    Set<String> checkConstraintValues = p.getDbCheckConstraintValues();
    if (checkConstraintValues != null) {
      if (beanDescriptor.hasInheritance()) {
        InheritInfo inheritInfo = beanDescriptor.getInheritInfo();
        inheritInfo.appendCheckConstraintValues(p.getName(), checkConstraintValues);
      }
      col.setCheckConstraint(buildCheckConstraint(p.getDbColumn(), checkConstraintValues));
      col.setCheckConstraintName(determineCheckConstraintName(col.getName()));
    }

    lastColumn = col;
    table.addColumn(col);
	}

  /**
   * Build the check constraint clause given the db column and values.
   */
  private String buildCheckConstraint(String dbColumn, Set<String> checkConstraintValues) {
    StringBuilder sb = new StringBuilder();
    sb.append("check ( ").append(dbColumn).append(" in (");
    int count = 0;
    for (String value : checkConstraintValues) {
      if (count++ > 0) {
        sb.append(",");
      }
      sb.append(value);
    }
    sb.append("))");
    return sb.toString();
  }

  private void indexSetAdd(String column) {
    indexSet.add(column);
  }

  private void indexSetAdd(List<MColumn> modelColumns) {
    String[] cols = new String[modelColumns.size()];
    for (int i = 0; i < modelColumns.size(); i++) {
      cols[i] = modelColumns.get(i).getName();
    }
    indexSet.add(cols);
  }

  /**
   * Return the primary key constraint name.
   */
  protected String determinePrimaryKeyName() {

    return ctx.primaryKeyName(table.getName());
  }

  /**
   * Return the foreign key constraint name given a single column foreign key.
   */
  protected String determineForeignKeyConstraintName(String columnName) {

    return ctx.foreignKeyConstraintName(table.getName(), columnName, ++countForeignKey);
  }

  protected String determineForeignKeyIndexName(String column) {

    String[] cols = {column};
    return determineForeignKeyIndexName(cols);
  }

  /**
   * Return the foreign key constraint name given a single column foreign key.
   */
  protected String determineForeignKeyIndexName(String[] columns) {

    return ctx.foreignKeyIndexName(table.getName(), columns, ++countIndex);
  }

  /**
   * Return the index name given a single column foreign key.
   */
  protected String determineIndexName(String column) {

    return ctx.indexName(table.getName(), column, ++countIndex);
  }

  /**
   * Return the index name given multiple columns.
   */
  protected String determineIndexName(String[] columns) {

    return ctx.indexName(table.getName(), columns, ++countIndex);
  }

  /**
   * Return the unique constraint name.
   */
  protected String determineUniqueConstraintName(String columnName) {

    return ctx.uniqueConstraintName(table.getName(), columnName, ++countUnique);
  }

  /**
   * Return the unique constraint name.
   */
  protected String determineUniqueConstraintName(String[] columnNames) {

    return ctx.uniqueConstraintName(table.getName(), columnNames, ++countUnique);
  }

  /**
   * Return the constraint name.
   */
  protected String determineCheckConstraintName(String columnName) {

    return ctx.checkConstraintName(table.getName(), columnName, ++countCheck);
  }


  private boolean hasValue(String val) {
    return val != null && !val.isEmpty();
  }

}