// SQLite3 Query Builder & Compiler

const QueryCompiler = require('../../../query/compiler');

const noop = require('../../../util/noop');
const constant = require('lodash/constant');
const each = require('lodash/each');
const identity = require('lodash/identity');
const isEmpty = require('lodash/isEmpty');
const reduce = require('lodash/reduce');
const { isString } = require('../../../util/is');

const emptyStr = constant('');

class QueryCompiler_SQLite3 extends QueryCompiler {
  constructor(client, builder) {
    super(client, builder);

    const { returning } = this.single;

    if (returning) {
      this.client.logger.warn(
        '.returning() is not supported by sqlite3 and will not have any effect.'
      );
    }

    // The locks are not applicable in SQLite3
    this.forShare = emptyStr;
    this.forUpdate = emptyStr;
  }

  // SQLite requires us to build the multi-row insert as a listing of select with
  // unions joining them together. So we'll build out this list of columns and
  // then join them all together with select unions to complete the queries.
  insert() {
    const insertValues = this.single.insert || [];
    let sql = this.with() + `insert into ${this.tableName} `;

    if (Array.isArray(insertValues)) {
      if (insertValues.length === 0) {
        return '';
      } else if (
        insertValues.length === 1 &&
        insertValues[0] &&
        isEmpty(insertValues[0])
      ) {
        return sql + this._emptyInsertValue;
      }
    } else if (typeof insertValues === 'object' && isEmpty(insertValues)) {
      return sql + this._emptyInsertValue;
    }

    const insertData = this._prepInsert(insertValues);

    if (isString(insertData)) {
      return sql + insertData;
    }

    if (insertData.columns.length === 0) {
      return '';
    }

    sql += `(${this.formatter.columnize(insertData.columns)})`;

    // backwards compatible error
    if (this.client.valueForUndefined !== null) {
      insertData.values.forEach((bindings) => {
        each(bindings, (binding) => {
          if (binding === undefined)
            throw new TypeError(
              '`sqlite` does not support inserting default values. Specify ' +
                'values explicitly or use the `useNullAsDefault` config flag. ' +
                '(see docs http://knexjs.org/#Builder-insert).'
            );
        });
      });
    }

    if (insertData.values.length === 1) {
      const parameters = this.formatter.parameterize(
        insertData.values[0],
        this.client.valueForUndefined
      );
      sql += ` values (${parameters})`;

      const { onConflict, ignore, merge } = this.single;
      if (onConflict && ignore) sql += this._ignore(onConflict);
      else if (onConflict && merge) {
        sql += this._merge(merge.updates, onConflict, insertValues);
        const wheres = this.where();
        if (wheres) sql += ` ${wheres}`;
      }

      return sql;
    }

    const blocks = [];
    let i = -1;
    while (++i < insertData.values.length) {
      let i2 = -1;
      const block = (blocks[i] = []);
      let current = insertData.values[i];
      current = current === undefined ? this.client.valueForUndefined : current;
      while (++i2 < insertData.columns.length) {
        block.push(
          this.formatter.alias(
            this.formatter.parameter(current[i2]),
            this.formatter.wrap(insertData.columns[i2])
          )
        );
      }
      blocks[i] = block.join(', ');
    }
    sql += ' select ' + blocks.join(' union all select ');

    const { onConflict, ignore, merge } = this.single;
    if (onConflict && ignore) sql += ' where true' + this._ignore(onConflict);
    else if (onConflict && merge) {
      sql +=
        ' where true' + this._merge(merge.updates, onConflict, insertValues);
    }

    return sql;
  }

  _ignore(columns) {
    return ` on conflict (${this.formatter.columnize(columns)}) do nothing`;
  }

  _merge(updates, columns, insert) {
    let sql = ` on conflict (${this.formatter.columnize(
      columns
    )}) do update set `;
    if (updates) {
      const updateData = this._prepUpdate(updates);
      if (typeof updateData === 'string') {
        sql += updateData;
      } else {
        sql += updateData.join(',');
      }

      return sql;
    } else {
      const insertData = this._prepInsert(insert);
      if (typeof insertData === 'string') {
        throw new Error(
          'If using merge with a raw insert query, then updates must be provided'
        );
      }

      sql += insertData.columns
        .map((column) => this.formatter.wrapString(column.split('.').pop()))
        .map((column) => `${column} = excluded.${column}`)
        .join(', ');

      return sql;
    }
  }

  // Compile a truncate table statement into SQL.
  truncate() {
    const { table } = this.single;
    return {
      sql: `delete from ${this.tableName}`,
      output() {
        return this.query({
          sql: `delete from sqlite_sequence where name = '${table}'`,
        }).catch(noop);
      },
    };
  }

  // Compiles a `columnInfo` query
  columnInfo() {
    const column = this.single.columnInfo;

    // The user may have specified a custom wrapIdentifier function in the config. We
    // need to run the identifiers through that function, but not format them as
    // identifiers otherwise.
    const table = this.client.customWrapIdentifier(this.single.table, identity);

    return {
      sql: `PRAGMA table_info(\`${table}\`)`,
      output(resp) {
        const maxLengthRegex = /.*\((\d+)\)/;
        const out = reduce(
          resp,
          function(columns, val) {
            let { type } = val;
            let maxLength = type.match(maxLengthRegex);
            if (maxLength) {
              maxLength = maxLength[1];
            }
            type = maxLength ? type.split('(')[0] : type;
            columns[val.name] = {
              type: type.toLowerCase(),
              maxLength,
              nullable: !val.notnull,
              defaultValue: val.dflt_value,
            };
            return columns;
          },
          {}
        );
        return (column && out[column]) || out;
      },
    };
  }

  limit() {
    const noLimit = !this.single.limit && this.single.limit !== 0;
    if (noLimit && !this.single.offset) return '';

    // Workaround for offset only,
    // see http://stackoverflow.com/questions/10491492/sqllite-with-skip-offset-only-not-limit
    return `limit ${this.formatter.parameter(
      noLimit ? -1 : this.single.limit
    )}`;
  }
}

module.exports = QueryCompiler_SQLite3;
