'use strict';

import {isString, isObject, isUndefined, isArray} from 'util';
import {QueryRaw, QuerySort, QueryWhere} from './interfaces';

/**
 * Query object allows to to programmatically build queries which can be returned
 * as JSON Object and used to fetch data from database, api endpoint etc.
 */
export class Query {

  protected _where: QueryWhere;
  protected _limit: number;
  protected _skip: number;
  protected _sort: QuerySort;
  protected _select: string[];

  /**
   * Create instance of Query.
   */
  constructor(query?: QueryRaw | Query) {
    if (!isUndefined(query)) {
      if (query instanceof Query) {
        query = (<Query>query).toJSON();
      }
    }
    const q: QueryRaw = <QueryRaw>(query || {});

    this._where = q.where || {};
    this._limit = q.limit || -1;
    this._skip = q.skip || 0;
    this._sort = q.sort || {};
    this._select = q.select || [];
  }

  protected _addCondition(field: string, condition: string, value: any): Query {
    if (isUndefined(this._where[field]) || isString(this._where[field])) {
      this._where[field] = {};
    }
    this._where[field][condition] = value;
    return this;
  }

  protected _sortItem(field, direction): QuerySort {
    const item: QuerySort = {};
    item[field] = direction;
    return item;
  }

  /**
   * Add condition which requires that field's value
   * to be equal to provided value
   */
  equal(field: string, value: any): Query {
    this._where[field] = value;
    return this;
  }

  /**
   * Add condition which requires that field's value
   * to be not equal to provided value
   */
  notEqual(field: string, value: any): Query {
    return this._addCondition(field, '$neq', value);
  }

  /**
   * Add condition which requires that field's value
   * to be less than provided value
   */
  lessThan(field: string, value: any): Query {
    return this._addCondition(field, '$lt', value);
  }

  /**
   * Add condition which requires that field's value
   * to be less than or equal to provided value
   */
  lessThanOrEqual(field: string, value: any): Query {
    return this._addCondition(field, '$lte', value);
  }

  /**
   * Add condition which requires that field's value
   * to be greater than provided value
   */
  greaterThan(field: string, value: any): Query {
    return this._addCondition(field, '$gt', value);
  }

  /**
   * Add condition which requires that field's value
   * to be greater than or equal to provided value
   */
  greaterThanOrEqual(field: string, value: any): Query {
    return this._addCondition(field, '$gte', value);
  }

  /**
   * Add condition which requires that field's value
   * to be contained in list of provided values
   */
  containedIn(field: string, values: any[]): Query {
    return this._addCondition(field, '$in', values);
  }

  /**
   * Add condition which requires that field's value
   * to be not contained in list of provided values
   */
  notContainedIn(field: string, values: any[]): Query {
    return this._addCondition(field, '$nin', values);
  }

  /**
   * Add condition which requires that field's value
   * match provided regular expression
   */
  regex(field: string, value: RegExp): Query {
    return this._addCondition(field, '$regex', value);
  }

  /**
   * Add condition which requires that field's value
   * contains provided value
   */
  contains(field: string, value: string): Query {
    return this._addCondition(field, '$regex', value);
  }

  /**
   * Add condition which requires that field's value
   * starts with provided value
   */
  startsWith(field: string, value: string): Query {
    return this._addCondition(field, '$regex', `^${value}`);
  }

  /**
   * Add condition which requires that field's value
   * ends with provided value
   */
  endsWith(field: string, value: string): Query {
    return this._addCondition(field, '$regex', `${value}$`);
  }

  /**
   * Set field's name which will be used for sort. Results
   * will be sort in ascending order
   */
  ascending(field: string): Query {
    Object.assign(this._sort, this._sortItem(field, 1));
    return this;
  }

  /**
   * Set field's name which will be used for sort. Results
   * will be sort in descending order
   */
  descending(field: string): Query {
    Object.assign(this._sort, this._sortItem(field, -1));
    return this;
  }

  /**
   * Set number of results to skip before return any result.
   */
  skip(skip: number): Query {
    this._skip = skip;
    return this;
  }

  /**
   * Set limit of results to return.
   */
  limit(limit: number): Query {
    this._limit = limit;
    return this;
  }

  /**
   * Set fields which will be returned.
   */
  select(fields: string[] | string): Query {
    if (!isArray(fields)) {
      fields = <string[]>[fields];
    }
    this._select = this._select.concat(<string[]>fields || []);
    return this;
  }

  /**
   * Concat two queries with OR
   */
  or(query: Query): Query {
    this._where = { $or: [this._where, query.toJSON().where] };
    return this;
  }

  /**
   * Return JSON Object
   */
  toJSON(): QueryRaw {
    const query: QueryRaw = {};

    if (Object.keys(this._where).length) {
      query.where = this._where;
    }
    if (this._skip) {
      query.skip = this._skip;
    }
    if (this._limit > 0) {
      query.limit = this._limit;
    }
    if (this._select.length > 0) {
      query.select = this._select;
    }
    if (Object.keys(this._sort).length) {
      query.sort = this._sort;
    }

    return Object.assign({}, query);
  }

  static create() {
    return new Query();
  }

  static from(input: any): Query {
    const query: QueryRaw = { where: {} };
    if (isString(input)) {
      try {
        input = JSON.parse(input);
      } catch (e) {
        throw new Error('Invalid input data for query.');
      }
    }
    if (isObject(input)) {
      if (input.where) {
        try {
          input.where = JSON.parse(input.where);
        } catch (e) {
          throw new Error('Invalid input data for query(where).');
        }
        if (isObject(input.where)) {
          query.where = input.where;
        }
      }
      if (input.limit) {
        query.limit = parseInt(input.limit);
      }
      if (input.skip) {
        query.skip = parseInt(input.skip);
      }
      if (input.sort) {
        try {
          input.sort = JSON.parse(input.sort);
        } catch (e) {
          throw new Error('Invalid input data for query(sort).');
        }
        if (isObject(input.sort)) {
          query.sort = {};
          query.sort = Object.keys(input.sort).reduce((s, c) => {
            s[c] = parseInt(input.sort[c]);
            return s;
          }, query.sort);
        }
      }
      if (input.select) {
        query.select = input.select;
      }
    }
    return new Query(query);
  }
}