const Promise = require('bluebird');

const { raw } = require('./RawBuilder');
const { Type: ValidationErrorType } = require('../model/ValidationError');
const { isObject, isString, isFunction, last, upperFirst } = require('../utils/objectUtils');

const QueryBuilderContext = require('./QueryBuilderContext');
const RelationExpression = require('./RelationExpression');
const QueryBuilderBase = require('./QueryBuilderBase');

const FindOperation = require('./operations/FindOperation');
const DeleteOperation = require('./operations/DeleteOperation');
const UpdateOperation = require('./operations/UpdateOperation');
const InsertOperation = require('./operations/InsertOperation');
const RelateOperation = require('./operations/RelateOperation');
const UnrelateOperation = require('./operations/UnrelateOperation');

const InsertGraphAndFetchOperation = require('./operations/InsertGraphAndFetchOperation');
const UpsertGraphAndFetchOperation = require('./operations/UpsertGraphAndFetchOperation');
const InsertAndFetchOperation = require('./operations/InsertAndFetchOperation');
const UpdateAndFetchOperation = require('./operations/UpdateAndFetchOperation');
const QueryBuilderOperation = require('./operations/QueryBuilderOperation');
const JoinRelationOperation = require('./operations/JoinRelationOperation');
const OnBuildKnexOperation = require('./operations/OnBuildKnexOperation');
const InsertGraphOperation = require('./operations/InsertGraphOperation');
const UpsertGraphOperation = require('./operations/UpsertGraphOperation');
const DeleteByIdOperation = require('./operations/DeleteByIdOperation');
const RunBeforeOperation = require('./operations/RunBeforeOperation');
const RunAfterOperation = require('./operations/RunAfterOperation');
const FindByIdOperation = require('./operations/FindByIdOperation');
const FindByIdsOperation = require('./operations/FindByIdsOperation');
const OnBuildOperation = require('./operations/OnBuildOperation');
const OnErrorOperation = require('./operations/OnErrorOperation');
const SelectOperation = require('./operations/select/SelectOperation');
const EagerOperation = require('./operations/eager/EagerOperation');
const RangeOperation = require('./operations/RangeOperation');
const FirstOperation = require('./operations/FirstOperation');
const FromOperation = require('./operations/FromOperation');
const KnexOperation = require('./operations/KnexOperation');

class QueryBuilder extends QueryBuilderBase {
  constructor(modelClass) {
    super(modelClass.knex());

    this._modelClass = modelClass;
    this._resultModelClass = null;
    this._explicitRejectValue = null;
    this._explicitResolveValue = null;

    this._eagerExpression = null;
    this._eagerFilters = null;
    this._eagerFiltersAtPath = [];
    this._allowedEagerExpression = null;
    this._allowedUpsertExpression = null;

    this._findOperationOptions = modelClass.defaultFindOptions;
    this._eagerOperationOptions = modelClass.defaultEagerOptions;

    this._findOperationFactory = findOperationFactory;
    this._insertOperationFactory = insertOperationFactory;
    this._updateOperationFactory = updateOperationFactory;
    this._patchOperationFactory = patchOperationFactory;
    this._relateOperationFactory = relateOperationFactory;
    this._unrelateOperationFactory = unrelateOperationFactory;
    this._deleteOperationFactory = deleteOperationFactory;
    this._eagerOperationFactory = modelClass.defaultEagerAlgorithm;
  }

  static forClass(modelClass) {
    return new this(modelClass);
  }

  static get QueryBuilderContext() {
    return QueryBuilderContext;
  }

  tableNameFor(modelClass, tableName) {
    const ctx = this.internalContext();
    const tableMap = ctx.tableMap;

    if (isString(tableName)) {
      ctx.tableMap = tableMap || new Map();
      ctx.tableMap.set(modelClass.getTableName(), tableName);
      return this;
    } else {
      return (tableMap && tableMap.get(modelClass.getTableName())) || modelClass.getTableName();
    }
  }

  aliasFor(modelClass, alias) {
    const ctx = this.internalContext();
    const aliasMap = ctx.aliasMap;

    if (isString(alias)) {
      ctx.aliasMap = aliasMap || new Map();
      ctx.aliasMap.set(modelClass.getTableName(), alias);
      return this;
    } else {
      return (aliasMap && aliasMap.get(modelClass.getTableName())) || null;
    }
  }

  alias(alias) {
    return this.aliasFor(this._modelClass, alias);
  }

  tableRefFor(modelClass) {
    return this.aliasFor(modelClass) || this.tableNameFor(modelClass);
  }

  subqueryOf(query) {
    const ctx = this.internalContext();

    // Merge alias and table name maps for subqueries.
    ctx.aliasMap = mergeMaps(query.internalContext().aliasMap, ctx.aliasMap);
    ctx.tableMap = mergeMaps(query.internalContext().tableMap, ctx.tableMap);

    return super.subqueryOf(query);
  }

  fullIdColumnFor(modelClass) {
    const tableName = this.tableRefFor(modelClass);
    const idColumn = modelClass.getIdColumn();

    if (Array.isArray(idColumn)) {
      const id = new Array(idColumn.length);

      for (let i = 0, l = idColumn.length; i < l; ++i) {
        id[i] = `${tableName}.${idColumn[i]}`;
      }

      return id;
    } else {
      return `${tableName}.${idColumn}`;
    }
  }

  applyFilter() {
    const namedFilters = this._modelClass.namedFilters;

    for (let i = 0, l = arguments.length; i < l; ++i) {
      const name = arguments[i];
      const filter = namedFilters[name];

      if (typeof filter !== 'function') {
        throw new Error(`Could not find filter "${name}".`);
      }

      filter(this);
    }

    return this;
  }

  modify() {
    return isString(arguments[0])
      ? this.applyFilter.apply(this, arguments)
      : super.modify.apply(this, arguments);
  }

  reject(error) {
    this._explicitRejectValue = error;
    return this;
  }

  resolve(value) {
    this._explicitResolveValue = value;
    return this;
  }

  isExecutable() {
    const hasExecutor = !!findQueryExecutorOperation(this);
    return !this._explicitRejectValue && !this._explicitResolveValue && !hasExecutor;
  }

  findOperationFactory(factory) {
    this._findOperationFactory = factory;
    return this;
  }

  insertOperationFactory(factory) {
    this._insertOperationFactory = factory;
    return this;
  }

  updateOperationFactory(factory) {
    this._updateOperationFactory = factory;
    return this;
  }

  patchOperationFactory(factory) {
    this._patchOperationFactory = factory;
    return this;
  }

  deleteOperationFactory(factory) {
    this._deleteOperationFactory = factory;
    return this;
  }

  relateOperationFactory(factory) {
    this._relateOperationFactory = factory;
    return this;
  }

  unrelateOperationFactory(factory) {
    this._unrelateOperationFactory = factory;
    return this;
  }

  eagerOperationFactory(factory) {
    if (arguments.length) {
      this._eagerOperationFactory = factory;
      return this;
    } else {
      return this._eagerOperationFactory;
    }
  }

  eagerAlgorithm(algorithm, eagerOptions) {
    this.eagerOperationFactory(algorithm);

    if (eagerOptions) {
      this.eagerOptions(eagerOptions);
    }

    return this;
  }

  eager(exp, filters) {
    this._eagerExpression = parseRelationExpression(this._modelClass, exp);
    this._eagerFilters = filters;

    checkEager(this);
    return this;
  }

  joinEager(exp, filters) {
    return this.eagerAlgorithm(this._modelClass.JoinEagerAlgorithm).eager(exp, filters);
  }

  naiveEager(exp, filters) {
    return this.eagerAlgorithm(this._modelClass.NaiveEagerAlgorithm).eager(exp, filters);
  }

  mergeEager(exp, filters) {
    if (!this._eagerExpression) {
      return this.eager(exp, filters);
    }

    this._eagerExpression = this._eagerExpression.merge(
      parseRelationExpression(this._modelClass, exp)
    );

    this._eagerFilters = Object.assign({}, this._eagerFilters, filters);

    checkEager(this);
    return this;
  }

  mergeJoinEager(exp, filters) {
    return this.eagerAlgorithm(this._modelClass.JoinEagerAlgorithm).mergeEager(exp, filters);
  }

  mergeNaiveEager(exp, filters) {
    return this.eagerAlgorithm(this._modelClass.NaiveEagerAlgorithm).mergeEager(exp, filters);
  }

  allowEager(exp) {
    this._allowedEagerExpression = parseRelationExpression(this._modelClass, exp);

    checkEager(this);
    return this;
  }

  allowedEagerExpression() {
    return this._allowedEagerExpression;
  }

  mergeAllowEager(exp) {
    if (!this._allowedEagerExpression) {
      return this.allowEager(exp);
    }

    this._allowedEagerExpression = this._allowedEagerExpression.merge(
      parseRelationExpression(this._modelClass, exp)
    );

    checkEager(this);
    return this;
  }

  modifyEager(path, filter) {
    this._eagerFiltersAtPath.push({ path, filter });
    return this;
  }

  filterEager() {
    return this.modifyEager.apply(this, arguments);
  }

  allowUpsert(exp) {
    this._allowedUpsertExpression = exp || null;

    if (isString(this._allowedUpsertExpression)) {
      this._allowedUpsertExpression = parseRelationExpression(
        this._modelClass,
        this._allowedUpsertExpression
      );
    }

    return this;
  }

  allowedUpsertExpression() {
    return this._allowedUpsertExpression;
  }

  allowInsert(exp) {
    return this.allowUpsert(exp);
  }

  eagerOptions(opt) {
    if (arguments.length !== 0) {
      this._eagerOperationOptions = Object.assign({}, this._eagerOperationOptions, opt);
      return this;
    } else {
      return this._eagerOperationOptions;
    }
  }

  findOptions(opt) {
    if (arguments.length !== 0) {
      this._findOperationOptions = Object.assign({}, this._findOperationOptions, opt);
      return this;
    } else {
      return this._findOperationOptions;
    }
  }

  modelClass() {
    return this._modelClass;
  }

  resultModelClass() {
    return this._resultModelClass || this.modelClass();
  }

  isFind() {
    return !(
      this.isInsert() ||
      this.isUpdate() ||
      this.isDelete() ||
      this.isRelate() ||
      this.isUnrelate()
    );
  }

  isInsert() {
    return this.has(InsertOperation);
  }

  isUpdate() {
    return this.has(UpdateOperation);
  }

  isDelete() {
    return this.has(DeleteOperation);
  }

  isRelate() {
    return this.has(RelateOperation);
  }

  isUnrelate() {
    return this.has(UnrelateOperation);
  }

  hasWheres() {
    return this.has(QueryBuilderBase.WhereSelector);
  }

  hasSelects() {
    return this.has(QueryBuilderBase.SelectSelector);
  }

  hasEager() {
    return !!this._eagerExpression;
  }

  isSelectAll() {
    if (this._operations.length === 0) {
      return true;
    }

    const tableRef = this.tableRefFor(this.modelClass());
    const tableName = this.tableNameFor(this.modelClass());

    return this._operations.every(op => {
      if (op.constructor === SelectOperation) {
        // SelectOperations with zero selections are the ones that only have
        // raw items or other non-trivial selections.
        return (
          op.selections.length > 0 &&
          op.selections.every(select => {
            return (!select.table || select.table === tableRef) && select.column === '*';
          })
        );
      } else if (op.constructor === FromOperation) {
        return op.table === tableName;
      } else if (op.name === 'as') {
        return true;
      } else {
        return false;
      }
    });
  }

  isFindQuery() {
    console.warn(
      `isFindQuery is deprecated. Use isFind instead. This method will be removed in version 2.0`
    );
    return this.isFind();
  }

  isEagerQuery() {
    console.warn(
      `isEagerQuery is deprecated. Use hasEager instead. This method will be removed in version 2.0`
    );
    return this.hasEager();
  }

  toString() {
    try {
      return this.build().toString();
    } catch (err) {
      return `This query cannot be built synchronously. Consider using debug() method instead.`;
    }
  }

  toSql() {
    return this.toString();
  }

  clone() {
    const builder = new this.constructor(this._modelClass);

    // Call the super class's clone implementation.
    this.baseCloneInto(builder);

    builder._resultModelClass = this._resultModelClass;

    builder._explicitRejectValue = this._explicitRejectValue;
    builder._explicitResolveValue = this._explicitResolveValue;

    builder._eagerExpression = this._eagerExpression;
    builder._eagerFilters = this._eagerFilters;
    builder._eagerFiltersAtPath = this._eagerFiltersAtPath.slice();

    builder._allowedEagerExpression = this._allowedEagerExpression;
    builder._allowedUpsertExpression = this._allowedUpsertExpression;

    builder._findOperationOptions = this._findOperationOptions;
    builder._eagerOperationOptions = this._eagerOperationOptions;

    builder._findOperationFactory = this._findOperationFactory;
    builder._insertOperationFactory = this._insertOperationFactory;
    builder._updateOperationFactory = this._updateOperationFactory;
    builder._patchOperationFactory = this._patchOperationFactory;
    builder._relateOperationFactory = this._relateOperationFactory;
    builder._unrelateOperationFactory = this._unrelateOperationFactory;
    builder._deleteOperationFactory = this._deleteOperationFactory;
    builder._eagerOperationFactory = this._eagerOperationFactory;

    return builder;
  }

  clearEager() {
    this._eagerExpression = null;
    this._eagerFilters = null;
    this._eagerFiltersAtPath = [];
    return this;
  }

  clearReject() {
    this._explicitRejectValue = null;
    return this;
  }

  clearResolve() {
    this._explicitResolveValue = null;
    return this;
  }

  castTo(modelClass) {
    this._resultModelClass = modelClass;
    return this;
  }

  then(successHandler, errorHandler) {
    const promise = this.execute();
    return promise.then.apply(promise, arguments);
  }

  map(mapper) {
    const promise = this.execute();
    return promise.map.apply(promise, arguments);
  }

  reduce(reducer, initialValue) {
    const promise = this.execute();
    return promise.reduce.apply(promise, arguments);
  }

  catch(errorHandler) {
    const promise = this.execute();
    return promise.catch.apply(promise, arguments);
  }

  return(returnValue) {
    const promise = this.execute();
    return promise.return.apply(promise, arguments);
  }

  reflect() {
    const promise = this.execute();
    return promise.reflect();
  }

  bind(context) {
    const promise = this.execute();
    return promise.bind.apply(promise, arguments);
  }

  asCallback(callback) {
    const promise = this.execute();
    return promise.asCallback.apply(promise, arguments);
  }

  nodeify(callback) {
    const promise = this.execute();
    return promise.nodeify.apply(promise, arguments);
  }

  resultSize() {
    const knex = this.knex();
    const builder = this.clone().clear(/orderBy|offset|limit/);

    const countQuery = knex.count('* as count').from(knexBuilder => {
      builder.build(knexBuilder).as('temp');
    });

    if (this.internalOptions().debug) {
      countQuery.debug();
    }

    return countQuery.then(result => (result[0] ? result[0].count : 0));
  }

  build(knexBuilder) {
    // Take a clone so that we don't modify this instance during build.
    const builder = this.clone();

    if (builder.isFind()) {
      // If no write operations have been called at this point this query is a
      // find query and we need to call the custom find implementation.
      addFindOperation(builder);
    }

    if (builder.hasEager()) {
      // If the query is an eager query, add the eager operation only at
      // this point of the query execution.
      addEagerFetchOperation(builder);
    }

    // We need to build the builder even if a query executor operation
    // has been called so that the onBuild hooks get called.
    knexBuilder = buildInto(builder, knexBuilder || builder.knex().queryBuilder());

    const queryExecutorOperation = findQueryExecutorOperation(builder);

    if (queryExecutorOperation) {
      // If the query executor is set, we build the builder that it returns.
      return queryExecutorOperation.queryExecutor(builder).build();
    } else {
      return knexBuilder;
    }
  }

  execute() {
    // Take a clone so that we don't modify this instance during execution.
    const builder = this.clone();

    return Promise.try(() => beforeExecute(builder))
      .then(() => doExecute(builder))
      .then(result => afterExecute(builder, result))
      .catch(error => handleExecuteError(builder, error));
  }

  pluck(propertyName) {
    return this.runAfter(result => {
      if (Array.isArray(result)) {
        return result.map(it => it && it[propertyName]);
      } else {
        return result;
      }
    });
  }

  throwIfNotFound() {
    return this.runAfter((result, builder) => {
      if (
        (Array.isArray(result) && result.length === 0) ||
        result === null ||
        result === undefined ||
        result === 0
      ) {
        throw this._modelClass.createNotFoundError(builder.context());
      } else {
        return result;
      }
    });
  }

  findSelection(selection, explicit) {
    explicit = explicit == null ? false : explicit;

    const table = this.tableRefFor(this._modelClass);
    let noSelectStatements = true;

    for (let i = 0, l = this._operations.length; i < l; ++i) {
      const op = this._operations[i];

      if (op.constructor === SelectOperation) {
        const selectionObj = op.findSelection(selection, table);
        noSelectStatements = false;

        if (selectionObj) {
          return selectionObj;
        }
      }
    }

    if (noSelectStatements && !explicit) {
      return SelectOperation.Selection.SelectAll;
    } else {
      return null;
    }
  }

  findAllSelections() {
    return this._operations
      .filter(op => op.is(SelectOperation))
      .reduce((selects, op) => selects.concat(op.selections), []);
  }

  hasSelection(selection, explicit) {
    return this.findSelection(selection, explicit) !== null;
  }

  hasSelectionAs(selection, alias, explicit) {
    const select = this.findSelection(selection, explicit);
    return select !== null && (select.column === '*' || select.name === alias);
  }

  traverse(modelClass, traverser) {
    if (typeof traverser === 'undefined') {
      traverser = modelClass;
      modelClass = null;
    }

    return this.runAfter(result => {
      this.resultModelClass().traverse(modelClass, result, traverser);
      return result;
    });
  }

  pick(modelClass, properties) {
    if (typeof properties === 'undefined') {
      properties = modelClass;
      modelClass = null;
    }

    // Turn the properties into a hash for performance.
    properties = properties.reduce((obj, prop) => {
      obj[prop] = true;
      return obj;
    }, {});

    return this.traverse(modelClass, model => {
      model.$pick(properties);
    });
  }

  omit(modelClass, properties) {
    if (typeof properties === 'undefined') {
      properties = modelClass;
      modelClass = null;
    }

    // Turn the properties into a hash for performance.
    properties = properties.reduce((obj, prop) => {
      obj[prop] = true;
      return obj;
    }, {});

    return this.traverse(modelClass, model => {
      model.$omit(properties);
    });
  }

  page(page, pageSize) {
    return this.range(page * pageSize, (page + 1) * pageSize - 1);
  }

  columnInfo({ table = null } = {}) {
    table = table || this.tableNameFor(this.modelClass());

    const knex = this.knex();
    const tableParts = table.split('.');
    const columnInfoQuery = knex(last(tableParts)).columnInfo();

    if (tableParts.length > 1) {
      columnInfoQuery.withSchema(tableParts[0]);
    }

    if (this.internalOptions().debug) {
      columnInfoQuery.debug();
    }

    return columnInfoQuery;
  }

  withSchema(schema) {
    this.internalContext().onBuild.push(builder => {
      if (!builder.has(/withSchema/)) {
        // Need to push this operation to the front because knex doesn't use the
        // schema for operations called before `withSchema`.
        builder.addOperationToFront(new KnexOperation('withSchema'), [schema]);
      }
    });

    return this;
  }

  debug /* istanbul ignore next */() {
    this.internalOptions().debug = true;
    this.internalContext().onBuild.push(builder => {
      builder.addOperation(new KnexOperation('debug'), []);
    });

    return this;
  }

  insert(modelsOrObjects) {
    return writeOperation(this, () => {
      const insertOperation = this._insertOperationFactory(this);

      this.addOperation(insertOperation, [modelsOrObjects]);
    });
  }

  insertAndFetch(modelsOrObjects) {
    return writeOperation(this, () => {
      const insertOperation = this._insertOperationFactory(this);

      const insertAndFetchOperation = new InsertAndFetchOperation('insertAndFetch', {
        delegate: insertOperation
      });

      this.addOperation(insertAndFetchOperation, [modelsOrObjects]);
    });
  }

  insertGraph(modelsOrObjects, opt) {
    return writeOperation(this, () => {
      const insertOperation = this._insertOperationFactory(this);

      const insertGraphOperation = new InsertGraphOperation('insertGraph', {
        delegate: insertOperation,
        opt
      });

      this.addOperation(insertGraphOperation, [modelsOrObjects]);
    });
  }

  insertWithRelated() {
    return this.insertGraph.apply(this, arguments);
  }

  insertGraphAndFetch(modelsOrObjects, opt) {
    return writeOperation(this, () => {
      const insertOperation = this._insertOperationFactory(this);

      const insertGraphOperation = new InsertGraphOperation('insertGraph', {
        delegate: insertOperation,
        opt
      });

      const insertGraphAndFetchOperation = new InsertGraphAndFetchOperation('insertGraphAndFetch', {
        delegate: insertGraphOperation
      });

      return this.addOperation(insertGraphAndFetchOperation, [modelsOrObjects]);
    });
  }

  insertWithRelatedAndFetch() {
    return this.insertGraphAndFetch.apply(this, arguments);
  }

  update(modelOrObject) {
    return writeOperation(this, () => {
      const updateOperation = this._updateOperationFactory(this);

      this.addOperation(updateOperation, [modelOrObject]);
    });
  }

  updateAndFetch(modelOrObject) {
    return writeOperation(this, () => {
      const updateOperation = this._updateOperationFactory(this);

      if (!(updateOperation.instance instanceof this._modelClass)) {
        throw new Error('updateAndFetch can only be called for instance operations');
      }

      const updateAndFetch = new UpdateAndFetchOperation('updateAndFetch', {
        delegate: updateOperation
      });

      // patchOperation is an instance update operation that already adds the
      // required "where id = $" clause.
      updateAndFetch.skipIdWhere = true;

      this.addOperation(updateAndFetch, [updateOperation.instance.$id(), modelOrObject]);
    });
  }

  updateAndFetchById(id, modelOrObject) {
    return writeOperation(this, () => {
      const updateOperation = this._updateOperationFactory(this);

      const updateAndFetch = new UpdateAndFetchOperation('updateAndFetch', {
        delegate: updateOperation
      });

      this.addOperation(updateAndFetch, [id, modelOrObject]);
    });
  }

  upsertGraph(modelsOrObjects, opt) {
    return writeOperation(this, () => {
      const insertOperation = this._insertOperationFactory(this);

      const upsertGraphOperation = new UpsertGraphOperation('upsertGraph', {
        delegate: insertOperation,
        opt
      });

      this.addOperation(upsertGraphOperation, [modelsOrObjects]);
    });
  }

  upsertGraphAndFetch(modelsOrObjects, opt) {
    return writeOperation(this, () => {
      const insertOperation = this._insertOperationFactory(this);

      const upsertGraphOperation = new UpsertGraphOperation('upsertGraph', {
        delegate: insertOperation,
        opt
      });

      const upsertGraphAndFetchOperation = new UpsertGraphAndFetchOperation('upsertGraphAndFetch', {
        delegate: upsertGraphOperation
      });

      return this.addOperation(upsertGraphAndFetchOperation, [modelsOrObjects]);
    });
  }

  patch(modelOrObject) {
    return writeOperation(this, () => {
      const patchOperation = this._patchOperationFactory(this);

      this.addOperation(patchOperation, [modelOrObject]);
    });
  }

  patchAndFetch(modelOrObject) {
    return writeOperation(this, () => {
      const patchOperation = this._patchOperationFactory(this);

      if (!(patchOperation.instance instanceof this._modelClass)) {
        throw new Error('patchAndFetch can only be called for instance operations');
      }

      const patchAndFetch = new UpdateAndFetchOperation('patchAndFetch', {
        delegate: patchOperation
      });

      // patchOperation is an instance update operation that already adds the
      // required "where id = $" clause.
      patchAndFetch.skipIdWhere = true;

      this.addOperation(patchAndFetch, [patchOperation.instance.$id(), modelOrObject]);
    });
  }

  patchAndFetchById(id, modelOrObject) {
    return writeOperation(this, () => {
      const patchOperation = this._patchOperationFactory(this);

      const patchAndFetch = new UpdateAndFetchOperation('patchAndFetch', {
        delegate: patchOperation
      });

      this.addOperation(patchAndFetch, [id, modelOrObject]);
    });
  }

  delete() {
    return writeOperation(this, () => {
      if (arguments.length) {
        throw new Error(
          `Don't pass arguments to delete(). You should use it like this: delete().where('foo', 'bar').andWhere(...)`
        );
      }

      const deleteOperation = this._deleteOperationFactory(this);
      this.addOperation(deleteOperation, arguments);
    });
  }

  del() {
    return this.delete.apply(this, arguments);
  }

  relate() {
    return writeOperation(this, () => {
      const relateOperation = this._relateOperationFactory(this);

      this.addOperation(relateOperation, arguments);
    });
  }

  unrelate() {
    return writeOperation(this, () => {
      if (arguments.length) {
        throw new Error(
          `Don't pass arguments to unrelate(). You should use it like this: unrelate().where('foo', 'bar').andWhere(...)`
        );
      }

      const unrelateOperation = this._unrelateOperationFactory(this);
      this.addOperation(unrelateOperation, arguments);
    });
  }

  increment(propertyName, howMuch) {
    const columnName = this.modelClass().propertyNameToColumnName(propertyName);

    return this.patch({
      [columnName]: raw('?? + ?', [columnName, howMuch])
    });
  }

  decrement(propertyName, howMuch) {
    const columnName = this.modelClass().propertyNameToColumnName(propertyName);

    return this.patch({
      [columnName]: raw('?? - ?', [columnName, howMuch])
    });
  }

  findOne() {
    return this.where.apply(this, arguments).first();
  }

  range() {
    return this.addOperation(new RangeOperation('range'), arguments);
  }

  first() {
    return this.addOperation(new FirstOperation('first'), arguments);
  }

  joinRelation() {
    return this.addOperation(
      new JoinRelationOperation('joinRelation', { joinOperation: 'join' }),
      arguments
    );
  }

  innerJoinRelation() {
    return this.addOperation(
      new JoinRelationOperation('innerJoinRelation', { joinOperation: 'innerJoin' }),
      arguments
    );
  }

  outerJoinRelation() {
    return this.addOperation(
      new JoinRelationOperation('outerJoinRelation', { joinOperation: 'outerJoin' }),
      arguments
    );
  }

  leftJoinRelation() {
    return this.addOperation(
      new JoinRelationOperation('leftJoinRelation', { joinOperation: 'leftJoin' }),
      arguments
    );
  }

  leftOuterJoinRelation() {
    return this.addOperation(
      new JoinRelationOperation('leftOuterJoinRelation', { joinOperation: 'leftOuterJoin' }),
      arguments
    );
  }

  rightJoinRelation() {
    return this.addOperation(
      new JoinRelationOperation('rightJoinRelation', { joinOperation: 'rightJoin' }),
      arguments
    );
  }

  rightOuterJoinRelation() {
    return this.addOperation(
      new JoinRelationOperation('rightOuterJoinRelation', { joinOperation: 'rightOuterJoin' }),
      arguments
    );
  }

  fullOuterJoinRelation() {
    return this.addOperation(
      new JoinRelationOperation('fullOuterJoinRelation', { joinOperation: 'fullOuterJoin' }),
      arguments
    );
  }

  deleteById() {
    return this.addOperation(new DeleteByIdOperation('deleteById'), arguments);
  }

  findById() {
    return this.addOperation(new FindByIdOperation('findById'), arguments);
  }

  findByIds() {
    return this.addOperation(new FindByIdsOperation('findByIds'), arguments);
  }

  runBefore() {
    return this.addOperation(new RunBeforeOperation('runBefore'), arguments);
  }

  onBuild() {
    return this.addOperation(new OnBuildOperation('onBuild'), arguments);
  }

  onBuildKnex() {
    return this.addOperation(new OnBuildKnexOperation('onBuildKnex'), arguments);
  }

  runAfter() {
    return this.addOperation(new RunAfterOperation('runAfter'), arguments);
  }

  onError() {
    return this.addOperation(new OnErrorOperation('onError'), arguments);
  }

  from() {
    return this.addOperation(new FromOperation('from'), arguments);
  }

  table() {
    return this.addOperation(new FromOperation('table'), arguments);
  }
}

Object.defineProperties(QueryBuilder.prototype, {
  isObjectionQueryBuilder: {
    enumerable: false,
    writable: false,
    value: true
  }
});

function parseRelationExpression(modelClass, exp) {
  try {
    return RelationExpression.create(exp);
  } catch (err) {
    throw modelClass.createValidationError({
      type: ValidationErrorType.RelationExpression,
      message: `Invalid relation expression "${exp}"`
    });
  }
}

function checkEager(builder) {
  const expr = builder._eagerExpression;
  const allowedExpr = builder._allowedEagerExpression;

  if (expr && allowedExpr && !allowedExpr.isSubExpression(expr)) {
    const modelClass = builder.modelClass();

    builder.reject(
      modelClass.createValidationError({
        type: ValidationErrorType.UnallowedRelation,
        message: 'eager expression not allowed'
      })
    );
  }
}

function findQueryExecutorOperation(builder) {
  for (let i = 0, l = builder._operations.length; i < l; ++i) {
    const op = builder._operations[i];

    if (op.hasQueryExecutor()) {
      return op;
    }
  }

  return null;
}

function beforeExecute(builder) {
  let promise = Promise.resolve();

  if (builder.isFind()) {
    // If no write operations have been called at this point this query is a
    // find query and we need to call the custom find implementation.
    addFindOperation(builder);
  }

  if (builder.hasEager()) {
    // If the query is an eager query, add the eager operation only at
    // this point of the query execution.
    addEagerFetchOperation(builder);
  }

  // Resolve all before hooks before building and executing the query
  // and the rest of the hooks.
  builder._operations.forEach(op => {
    if (op.hasOnBefore1()) {
      promise = promise.then(result => op.onBefore1(builder, result));
    }
  });

  promise = chainHooks(promise, builder, builder.context().runBefore);
  promise = chainHooks(promise, builder, builder.internalContext().runBefore);

  builder._operations.forEach(op => {
    if (op.hasOnBefore2()) {
      promise = promise.then(result => op.onBefore2(builder, result));
    }
  });

  builder._operations.forEach(op => {
    if (op.hasOnBefore3()) {
      promise = promise.then(result => op.onBefore3(builder, result));
    }
  });

  return promise;
}

function doExecute(builder) {
  let promise = Promise.resolve();

  const knexBuilder = buildInto(builder, builder.knex().queryBuilder());
  const queryExecutorOperation = findQueryExecutorOperation(builder);

  const explicitRejectValue = builder._explicitRejectValue;
  const explicitResolveValue = builder._explicitResolveValue;

  if (explicitRejectValue !== null) {
    promise = Promise.reject(explicitRejectValue);
  } else if (explicitResolveValue !== null) {
    promise = Promise.resolve(explicitResolveValue);
  } else if (queryExecutorOperation !== null) {
    promise = Promise.resolve(queryExecutorOperation.queryExecutor(builder));
  } else {
    promise = Promise.resolve(knexBuilder);

    builder._operations.forEach(op => {
      if (op.hasOnRawResult()) {
        promise = promise.then(result => op.onRawResult(builder, result));
      }
    });

    promise = promise.then(result => createModels(result, builder));
  }

  return promise;
}

function afterExecute(builder, result) {
  let promise = Promise.resolve(result);

  builder._operations.forEach(op => {
    if (op.hasOnAfter1()) {
      promise = promise.then(result => op.onAfter1(builder, result));
    }
  });

  builder._operations.forEach(op => {
    if (op.hasOnAfter2()) {
      promise = promise.then(result => op.onAfter2(builder, result));
    }
  });

  promise = chainHooks(promise, builder, builder.context().runAfter);
  promise = chainHooks(promise, builder, builder.internalContext().runAfter);

  builder._operations.forEach(op => {
    if (op.hasOnAfter3()) {
      promise = promise.then(result => op.onAfter3(builder, result));
    }
  });

  return promise;
}

function handleExecuteError(builder, err) {
  let promise = Promise.reject(err);

  builder._operations.forEach(op => {
    if (op.hasOnError()) {
      promise = promise.catch(err => op.onError(builder, err));
    }
  });

  return promise;
}

function addFindOperation(builder) {
  if (!builder.has(FindOperation)) {
    const operation = builder._findOperationFactory(builder);
    builder.addOperationToFront(operation, []);
  }
}

function addEagerFetchOperation(builder) {
  if (!builder.has(EagerOperation) && builder._eagerExpression) {
    const operation = builder._eagerOperationFactory(builder);
    const expression = builder._eagerExpression.clone();
    const filters = Object.assign({}, builder._eagerFilters);

    builder._eagerFiltersAtPath.forEach((filter, i) => {
      const filterName = `_f${i}_`;

      expression.expressionsAtPath(filter.path).forEach(expr => {
        expr.rawNode.$modify.push(filterName);
      });

      filters[filterName] = filter.filter;
    });

    builder.addOperation(operation, [expression, filters]);
  }
}

function buildInto(builder, knexBuilder) {
  callOnBuildHooks(builder, builder.context().onBuild);
  callOnBuildHooks(builder, builder.internalContext().onBuild);

  // Call super class build.
  knexBuilder = builder.buildInto(knexBuilder);

  const fromOperation = builder.findLastOperation(QueryBuilderBase.FromSelector);
  const hasSelects = builder.has(QueryBuilderBase.SelectSelector);

  // Set the table only if it hasn't been explicitly set yet.
  if (!fromOperation) {
    setDefaultTable(builder, knexBuilder);
  }

  // Only add `table.*` select if there are no explicit selects
  // and `from` is a table name and not a subquery.
  if (!hasSelects && (!fromOperation || fromOperation.table)) {
    setDefaultSelect(builder, knexBuilder);
  }

  return knexBuilder;
}

function callOnBuildHooks(builder, func) {
  if (isFunction(func)) {
    func.call(builder, builder);
  } else if (Array.isArray(func)) {
    func.forEach(func => callOnBuildHooks(builder, func));
  }
}

function setDefaultTable(builder, knexBuilder) {
  const table = builder.tableNameFor(builder.modelClass());
  const tableRef = builder.tableRefFor(builder.modelClass());

  if (table === tableRef) {
    knexBuilder.table(table);
  } else {
    knexBuilder.table(`${table} as ${tableRef}`);
  }
}

function setDefaultSelect(builder, knexBuilder) {
  const tableRef = builder.tableRefFor(builder.modelClass());

  knexBuilder.select(`${tableRef}.*`);
}

function chainHooks(promise, builder, func) {
  if (isFunction(func)) {
    promise = promise.then(result => func.call(builder, result, builder));
  } else if (Array.isArray(func)) {
    func.forEach(func => {
      promise = chainHooks(promise, builder, func);
    });
  }

  return promise;
}

function createModels(result, builder) {
  const modelClass = builder.resultModelClass();

  if (result === null || result === undefined) {
    return null;
  }

  if (Array.isArray(result)) {
    if (result.length && shouldBeConvertedToModel(result[0], modelClass)) {
      for (let i = 0, l = result.length; i < l; ++i) {
        result[i] = modelClass.fromDatabaseJson(result[i]);
      }
    }
  } else if (shouldBeConvertedToModel(result, modelClass)) {
    result = modelClass.fromDatabaseJson(result);
  }

  return result;
}

function shouldBeConvertedToModel(obj, modelClass) {
  return isObject(obj) && !(obj instanceof modelClass);
}

function writeOperation(builder, cb) {
  if (!builder.isFind()) {
    return builder.reject(
      new Error(
        'Double call to a write method. ' +
          'You can only call one of the write methods ' +
          '(insert, update, patch, delete, relate, unrelate, increment, decrement) ' +
          'and only once per query builder.'
      )
    );
  }

  try {
    cb();
    return builder;
  } catch (err) {
    return builder.reject(err);
  }
}

function mergeMaps(map1, map2) {
  const map = new Map();

  if (map1) {
    for (let key of map1.keys()) {
      map.set(key, map1.get(key));
    }
  }

  if (map2) {
    for (let key of map2.keys()) {
      map.set(key, map2.get(key));
    }
  }

  return map;
}

function findOperationFactory() {
  return new FindOperation('find');
}

function insertOperationFactory() {
  return new InsertOperation('insert');
}

function updateOperationFactory() {
  return new UpdateOperation('update');
}

function patchOperationFactory() {
  return new UpdateOperation('patch', {
    modelOptions: { patch: true }
  });
}

function relateOperationFactory() {
  return new RelateOperation('relate', {});
}

function unrelateOperationFactory() {
  return new UnrelateOperation('unrelate', {});
}

function deleteOperationFactory() {
  return new DeleteOperation('delete');
}

module.exports = QueryBuilder;
