const assert = require('assert');

// JoinClause
// -------

function getClauseFromArguments(compilerType, bool, first, operator, second) {
  if (typeof first === 'function') {
    return {
      type: 'onWrapped',
      value: first,
      bool: bool,
    };
  }

  switch (arguments.length) {
    case 3:
      return { type: 'onRaw', value: first, bool };
    case 4:
      return {
        type: compilerType,
        column: first,
        operator: '=',
        value: operator,
        bool,
      };
    default:
      return {
        type: compilerType,
        column: first,
        operator,
        value: second,
        bool,
      };
  }
}

// The "JoinClause" is an object holding any necessary info about a join,
// including the type, and any associated tables & columns being joined.
class JoinClause {
  constructor(table, type, schema) {
    this.schema = schema;
    this.table = table;
    this.joinType = type;
    this.and = this;
    this.clauses = [];
  }

  get or() {
    return this._bool('or');
  }

  // Adds an "on" clause to the current join object.
  on(first) {
    if (typeof first === 'object' && typeof first.toSQL !== 'function') {
      const keys = Object.keys(first);
      let i = -1;
      const method = this._bool() === 'or' ? 'orOn' : 'on';
      while (++i < keys.length) {
        this[method](keys[i], first[keys[i]]);
      }
      return this;
    }

    const data = getClauseFromArguments('onBasic', this._bool(), ...arguments);

    if (data) {
      this.clauses.push(data);
    }

    return this;
  }

  // Adds an "or on" clause to the current join object.
  orOn(first, operator, second) {
    return this._bool('or').on.apply(this, arguments);
  }

  onJsonPathEquals(columnFirst, jsonPathFirst, columnSecond, jsonPathSecond) {
    this.clauses.push({
      type: 'onJsonPathEquals',
      columnFirst: columnFirst,
      jsonPathFirst: jsonPathFirst,
      columnSecond: columnSecond,
      jsonPathSecond: jsonPathSecond,
      bool: this._bool(),
      not: this._not(),
    });
    return this;
  }

  orOnJsonPathEquals(columnFirst, jsonPathFirst, columnSecond, jsonPathSecond) {
    return this._bool('or').onJsonPathEquals.apply(this, arguments);
  }

  // Adds a "using" clause to the current join.
  using(column) {
    return this.clauses.push({ type: 'onUsing', column, bool: this._bool() });
  }

  onVal(first) {
    if (typeof first === 'object' && typeof first.toSQL !== 'function') {
      const keys = Object.keys(first);
      let i = -1;
      const method = this._bool() === 'or' ? 'orOnVal' : 'onVal';
      while (++i < keys.length) {
        this[method](keys[i], first[keys[i]]);
      }
      return this;
    }

    const data = getClauseFromArguments('onVal', this._bool(), ...arguments);

    if (data) {
      this.clauses.push(data);
    }

    return this;
  }

  andOnVal() {
    return this.onVal(...arguments);
  }

  orOnVal() {
    return this._bool('or').onVal(...arguments);
  }

  onBetween(column, values) {
    assert(
      Array.isArray(values),
      'The second argument to onBetween must be an array.'
    );
    assert(
      values.length === 2,
      'You must specify 2 values for the onBetween clause'
    );
    this.clauses.push({
      type: 'onBetween',
      column,
      value: values,
      bool: this._bool(),
      not: this._not(),
    });
    return this;
  }

  onNotBetween(column, values) {
    return this._not(true).onBetween(column, values);
  }

  orOnBetween(column, values) {
    return this._bool('or').onBetween(column, values);
  }

  orOnNotBetween(column, values) {
    return this._bool('or')._not(true).onBetween(column, values);
  }

  onIn(column, values) {
    if (Array.isArray(values) && values.length === 0) return this.on(1, '=', 0);
    this.clauses.push({
      type: 'onIn',
      column,
      value: values,
      not: this._not(),
      bool: this._bool(),
    });
    return this;
  }

  onNotIn(column, values) {
    return this._not(true).onIn(column, values);
  }

  orOnIn(column, values) {
    return this._bool('or').onIn(column, values);
  }

  orOnNotIn(column, values) {
    return this._bool('or')._not(true).onIn(column, values);
  }

  onNull(column) {
    this.clauses.push({
      type: 'onNull',
      column,
      not: this._not(),
      bool: this._bool(),
    });
    return this;
  }

  orOnNull(callback) {
    return this._bool('or').onNull(callback);
  }

  onNotNull(callback) {
    return this._not(true).onNull(callback);
  }

  orOnNotNull(callback) {
    return this._not(true)._bool('or').onNull(callback);
  }

  onExists(callback) {
    this.clauses.push({
      type: 'onExists',
      value: callback,
      not: this._not(),
      bool: this._bool(),
    });
    return this;
  }

  orOnExists(callback) {
    return this._bool('or').onExists(callback);
  }

  onNotExists(callback) {
    return this._not(true).onExists(callback);
  }

  orOnNotExists(callback) {
    return this._not(true)._bool('or').onExists(callback);
  }

  // Explicitly set the type of join, useful within a function when creating a grouped join.
  type(type) {
    this.joinType = type;
    return this;
  }

  _bool(bool) {
    if (arguments.length === 1) {
      this._boolFlag = bool;
      return this;
    }
    const ret = this._boolFlag || 'and';
    this._boolFlag = 'and';
    return ret;
  }

  _not(val) {
    if (arguments.length === 1) {
      this._notFlag = val;
      return this;
    }
    const ret = this._notFlag;
    this._notFlag = false;
    return ret;
  }
}

Object.assign(JoinClause.prototype, {
  grouping: 'join',
});

JoinClause.prototype.andOn = JoinClause.prototype.on;
JoinClause.prototype.andOnIn = JoinClause.prototype.onIn;
JoinClause.prototype.andOnNotIn = JoinClause.prototype.onNotIn;
JoinClause.prototype.andOnNull = JoinClause.prototype.onNull;
JoinClause.prototype.andOnNotNull = JoinClause.prototype.onNotNull;
JoinClause.prototype.andOnExists = JoinClause.prototype.onExists;
JoinClause.prototype.andOnNotExists = JoinClause.prototype.onNotExists;
JoinClause.prototype.andOnBetween = JoinClause.prototype.onBetween;
JoinClause.prototype.andOnNotBetween = JoinClause.prototype.onNotBetween;
JoinClause.prototype.andOnJsonPathEquals =
  JoinClause.prototype.onJsonPathEquals;

module.exports = JoinClause;
