const filter = require('lodash/filter');
const identity = require('lodash/identity');
const { isObject } = require('../../../util/is');
const { normalizeArr } = require('../../../util/helpers');

const TableCompiler = require('../../../schema/tablecompiler');
const { formatDefault } = require('../../../formatter/formatterUtils');

class TableCompiler_SQLite3 extends TableCompiler {
  constructor() {
    super(...arguments);
  }

  // Create a new table.
  createQuery(columns, ifNot, like) {
    const createStatement = ifNot
      ? 'create table if not exists '
      : 'create table ';

    let sql = createStatement + this.tableName();

    if (like && this.tableNameLike()) {
      sql += ' as select * from ' + this.tableNameLike() + ' where 0=1';
    } else {
      // so we will need to check for a primary key commands and add the columns
      // to the table's declaration here so they can be created on the tables.
      sql += ' (' + columns.sql.join(', ');
      sql += this.foreignKeys() || '';
      sql += this.primaryKeys() || '';
      sql += this._addChecks();
      sql += ')';
    }
    this.pushQuery(sql);

    if (like) {
      this.addColumns(columns, this.addColumnsPrefix);
    }
  }

  addColumns(columns, prefix, colCompilers) {
    if (prefix === this.alterColumnsPrefix) {
      const compiler = this;

      const columnsInfo = colCompilers.map((col) => {
        const name = this.client.customWrapIdentifier(
          col.getColumnName(),
          identity,
          col.columnBuilder.queryContext()
        );

        const type = col.getColumnType();

        const defaultTo = col.modified['defaultTo']
          ? formatDefault(col.modified['defaultTo'][0], col.type, this.client)
          : null;

        const notNull =
          col.modified['nullable'] && col.modified['nullable'][0] === false;

        return { name, type, defaultTo, notNull };
      });

      this.pushQuery({
        sql: `PRAGMA table_info(${this.tableName()})`,
        statementsProducer(pragma, connection) {
          return compiler.client
            .ddl(compiler, pragma, connection)
            .alterColumn(columnsInfo);
        },
      });
    } else {
      for (let i = 0, l = columns.sql.length; i < l; i++) {
        this.pushQuery({
          sql: `alter table ${this.tableName()} add column ${columns.sql[i]}`,
          bindings: columns.bindings[i],
        });
      }
    }
  }

  // Compile a drop unique key command.
  dropUnique(columns, indexName) {
    indexName = indexName
      ? this.formatter.wrap(indexName)
      : this._indexCommand('unique', this.tableNameRaw, columns);
    this.pushQuery(`drop index ${indexName}`);
  }

  // Compile a drop unique key command if one exists.
  dropUniqueIfExists(columns, indexName) {
    indexName = indexName
      ? this.formatter.wrap(indexName)
      : this._indexCommand('unique', this.tableNameRaw, columns);
    this.pushQuery(`drop index if exists ${indexName}`);
  }

  // Compile a drop foreign key command.
  dropForeign(columns, indexName) {
    const compiler = this;

    columns = Array.isArray(columns) ? columns : [columns];
    columns = columns.map((column) =>
      this.client.customWrapIdentifier(column, identity)
    );
    indexName = this.client.customWrapIdentifier(indexName, identity);

    this.pushQuery({
      sql: `PRAGMA table_info(${this.tableName()})`,
      output(pragma) {
        return compiler.client
          .ddl(compiler, pragma, this.connection)
          .dropForeign(columns, indexName);
      },
    });
  }

  dropForeignIfExists() {
    throw new Error('.dropForeignIfExists is not supported for sqlite3');
  }

  // Compile a drop primary key command.
  dropPrimary(constraintName) {
    const compiler = this;

    constraintName = this.client.customWrapIdentifier(constraintName, identity);

    this.pushQuery({
      sql: `PRAGMA table_info(${this.tableName()})`,
      output(pragma) {
        return compiler.client
          .ddl(compiler, pragma, this.connection)
          .dropPrimary(constraintName);
      },
    });
  }

  dropPrimaryIfExists() {
    throw new Error('.dropPrimaryIfExists is not supported for sqlite3');
  }

  dropIndex(columns, indexName) {
    indexName = indexName
      ? this.formatter.wrap(indexName)
      : this._indexCommand('index', this.tableNameRaw, columns);
    this.pushQuery(`drop index ${indexName}`);
  }

  // Compile a unique key command.
  unique(columns, indexName) {
    let deferrable;
    let predicate;
    if (isObject(indexName)) {
      ({ indexName, deferrable, predicate } = indexName);
    }
    if (deferrable && deferrable !== 'not deferrable') {
      this.client.logger.warn(
        `sqlite3: unique index \`${indexName}\` will not be deferrable ${deferrable} because sqlite3 does not support deferred constraints.`
      );
    }
    indexName = indexName
      ? this.formatter.wrap(indexName)
      : this._indexCommand('unique', this.tableNameRaw, columns);
    columns = this.formatter.columnize(columns);

    const predicateQuery = predicate
      ? ' ' + this.client.queryCompiler(predicate).where()
      : '';

    this.pushQuery(
      `create unique index ${indexName} on ${this.tableName()} (${columns})${predicateQuery}`
    );
  }

  // Compile a plain index key command.
  index(columns, indexName, options) {
    indexName = indexName
      ? this.formatter.wrap(indexName)
      : this._indexCommand('index', this.tableNameRaw, columns);
    columns = this.formatter.columnize(columns);

    let predicate;
    if (isObject(options)) {
      ({ predicate } = options);
    }
    const predicateQuery = predicate
      ? ' ' + this.client.queryCompiler(predicate).where()
      : '';
    this.pushQuery(
      `create index ${indexName} on ${this.tableName()} (${columns})${predicateQuery}`
    );
  }

  /**
   * Add a primary key to an existing table.
   *
   * @NOTE The `createQuery` method above handles table creation. Don't do anything regarding table
   *       creation in this method
   *
   * @param {string | string[]} columns - Column name(s) to assign as primary keys
   * @param {string} [constraintName] - Custom name for the PK constraint
   */
  primary(columns, constraintName) {
    const compiler = this;

    columns = Array.isArray(columns) ? columns : [columns];
    columns = columns.map((column) =>
      this.client.customWrapIdentifier(column, identity)
    );

    let deferrable;
    if (isObject(constraintName)) {
      ({ constraintName, deferrable } = constraintName);
    }
    if (deferrable && deferrable !== 'not deferrable') {
      this.client.logger.warn(
        `sqlite3: primary key constraint \`${constraintName}\` will not be deferrable ${deferrable} because sqlite3 does not support deferred constraints.`
      );
    }
    constraintName = this.client.customWrapIdentifier(constraintName, identity);

    if (this.method !== 'create' && this.method !== 'createIfNot') {
      this.pushQuery({
        sql: `PRAGMA table_info(${this.tableName()})`,
        output(pragma) {
          return compiler.client
            .ddl(compiler, pragma, this.connection)
            .primary(columns, constraintName);
        },
      });
    }
  }

  /**
   * Add a foreign key constraint to an existing table
   *
   * @NOTE The `createQuery` method above handles foreign key constraints on table creation. Don't do
   *       anything regarding table creation in this method
   *
   * @param {object} foreignInfo - Information about the current column foreign setup
   * @param {string | string[]} [foreignInfo.column] - Column in the current constraint
   * @param {string | undefined} foreignInfo.keyName - Name of the foreign key constraint
   * @param {string | string[]} foreignInfo.references - What column it references in the other table
   * @param {string} foreignInfo.inTable - What table is referenced in this constraint
   * @param {string} [foreignInfo.onUpdate] - What to do on updates
   * @param {string} [foreignInfo.onDelete] - What to do on deletions
   */
  foreign(foreignInfo) {
    const compiler = this;

    if (this.method !== 'create' && this.method !== 'createIfNot') {
      foreignInfo.column = Array.isArray(foreignInfo.column)
        ? foreignInfo.column
        : [foreignInfo.column];
      foreignInfo.column = foreignInfo.column.map((column) =>
        this.client.customWrapIdentifier(column, identity)
      );
      foreignInfo.inTable = this.client.customWrapIdentifier(
        foreignInfo.inTable,
        identity
      );
      foreignInfo.references = Array.isArray(foreignInfo.references)
        ? foreignInfo.references
        : [foreignInfo.references];
      foreignInfo.references = foreignInfo.references.map((column) =>
        this.client.customWrapIdentifier(column, identity)
      );

      this.pushQuery({
        sql: `PRAGMA table_info(${this.tableName()})`,
        statementsProducer(pragma, connection) {
          return compiler.client
            .ddl(compiler, pragma, connection)
            .foreign(foreignInfo);
        },
      });
    }
  }

  primaryKeys() {
    const pks = filter(this.grouped.alterTable || [], { method: 'primary' });
    if (pks.length > 0 && pks[0].args.length > 0) {
      const columns = pks[0].args[0];
      let constraintName = pks[0].args[1] || '';
      if (constraintName) {
        constraintName = ' constraint ' + this.formatter.wrap(constraintName);
      }
      const needUniqueCols =
        this.grouped.columns.filter((t) => t.builder._type === 'increments')
          .length > 0;
      // SQLite dont support autoincrement columns and composite primary keys (autoincrement is always primary key).
      // You need to add unique index instead when you have autoincrement columns (https://stackoverflow.com/a/6154876/1535159)
      return `,${constraintName} ${
        needUniqueCols ? 'unique' : 'primary key'
      } (${this.formatter.columnize(columns)})`;
    }
  }

  foreignKeys() {
    let sql = '';
    const foreignKeys = filter(this.grouped.alterTable || [], {
      method: 'foreign',
    });
    for (let i = 0, l = foreignKeys.length; i < l; i++) {
      const foreign = foreignKeys[i].args[0];
      const column = this.formatter.columnize(foreign.column);
      const references = this.formatter.columnize(foreign.references);
      const foreignTable = this.formatter.wrap(foreign.inTable);
      let constraintName = foreign.keyName || '';
      if (constraintName) {
        constraintName = ' constraint ' + this.formatter.wrap(constraintName);
      }
      sql += `,${constraintName} foreign key(${column}) references ${foreignTable}(${references})`;
      if (foreign.onDelete) sql += ` on delete ${foreign.onDelete}`;
      if (foreign.onUpdate) sql += ` on update ${foreign.onUpdate}`;
    }
    return sql;
  }

  createTableBlock() {
    return this.getColumns().concat().join(',');
  }

  renameColumn(from, to) {
    this.pushQuery({
      sql: `alter table ${this.tableName()} rename ${this.formatter.wrap(
        from
      )} to ${this.formatter.wrap(to)}`,
    });
  }

  _setNullableState(column, isNullable) {
    const compiler = this;

    this.pushQuery({
      sql: `PRAGMA table_info(${this.tableName()})`,
      statementsProducer(pragma, connection) {
        return compiler.client
          .ddl(compiler, pragma, connection)
          .setNullable(column, isNullable);
      },
    });
  }

  dropColumn() {
    const compiler = this;
    // Accept either an array of column names or varargs.
    const columns = normalizeArr(...arguments);

    const columnsWrapped = columns.map((column) =>
      this.client.customWrapIdentifier(column, identity)
    );

    this.pushQuery({
      sql: `PRAGMA table_info(${this.tableName()})`,
      output(pragma) {
        return compiler.client
          .ddl(compiler, pragma, this.connection)
          .dropColumn(columnsWrapped);
      },
    });
  }
}

module.exports = TableCompiler_SQLite3;
