import { Model, IModelSetOptions } from './Model';
import { Assert } from '../misc/Assert';
import { IStringMap } from '../rest/GenericParam';
import * as _ from 'underscore';
import { Utils } from '../utils/Utils';

export const QUERY_STATE_ATTRIBUTES = {
  Q: 'q',
  FIRST: 'first',
  T: 't',
  TG: 'tg',
  SORT: 'sort',
  LAYOUT: 'layout',
  HD: 'hd',
  HQ: 'hq',
  QUICKVIEW: 'quickview',
  DEBUG: 'debug',
  NUMBER_OF_RESULTS: 'numberOfResults',
  MISSING_TERMS: 'missingTerms'
};

export interface IQueryStateIncludedAttribute {
  title: string;
  included: string[];
}

export interface IQueryStateExcludedAttribute {
  title: string;
  excluded: string[];
}

/**
 * The `QueryStateModel` class is a key-value store which contains the current state of the components that can affect
 * the query (see [State](https://docs.coveo.com/en/344/)). This class inherits from the [`Model`](https://coveo.github.io/search-ui/classes/model.html)
 * class. Optionally, it is possible to persist the state in the query string in order to enable browser history
 * management (see the [`HistoryController`]{@link HistoryController} class).
 *
 * Components set values in the `QueryStateModel` instance to reflect their current state. The `QueryStateModel`
 * triggers state events (see [`eventTypes`]{@link Model.eventTypes}) whenever one of its values is modified. Components
 * listen to triggered state events to update themselves when appropriate.
 *
 * For instance, when a query is triggered, the [`Searchbox`]{@link Searchbox} component sets the `q` attribute (the
 * basic query expression), while the [`Pager`]{@link Pager} component sets the `first` attribute (the index of the
 * first result to display in the result list), and so on.
 *
 * **Example:**
 *
 * > The user modifies the content of the `Searchbox` and submits a query. This triggers the following state events:
 * > - `state:change:q` (because the value of `q` has changed).
 * > - `state:change` (because at least one value has changed in the `QueryStateModel`).
 * >
 * > Components or external code can attach handlers to those events:
 * > ```javascript
 * > Coveo.$$(document).on('state:change:q', function() {
 * >   [ ... ]
 * > });
 * > ```
 *
 * **Note:**
 * > Normally, you should interact with the `QueryStateModel` instance using the [`Coveo.state`]{@link state} top-level
 * > function.
 */
export class QueryStateModel extends Model {
  static ID = 'state';

  static defaultAttributes = {
    q: '',
    first: 0,
    fv: '',
    t: '',
    hd: '',
    hq: '',
    sort: '',
    layout: 'list',
    tg: '',
    quickview: '',
    debug: false,
    numberOfResults: 10,
    missingTerms: []
  };

  static attributesEnum = {
    q: 'q',
    first: 'first',
    fv: 'fv',
    t: 't',
    sort: 'sort',
    layout: 'layout',
    hd: 'hd',
    hq: 'hq',
    tg: 'tg',
    quickview: 'quickview',
    debug: 'debug',
    numberOfResults: 'numberOfResults',
    missingTerms: 'missingTerms'
  };

  static getFacetId(id: string, include: boolean = true) {
    return 'f:' + id + (include ? '' : ':not');
  }

  static getFacetOperator(id: string) {
    return 'f:' + id + ':operator';
  }

  static getFacetLookupValue(id: string) {
    return QueryStateModel.getFacetId(id) + ':lookupvalues';
  }

  /**
   * Creates a new `QueryStateModel` instance.
   * @param element The HTMLElement on which to instantiate the `QueryStateModel`.
   * @param attributes The state key-value store to instantiate the `QueryStateModel` with.
   */
  constructor(element: HTMLElement, attributes?: IStringMap<string>) {
    const merged = { ...QueryStateModel.defaultAttributes, ...attributes };
    super(element, QueryStateModel.ID, merged);
  }

  /**
   * Validates whether at least one facet is currently active (has selected or excluded values) in the interface.
   *
   * @returns {boolean} `true` if at least one facet is active; `false` otherwise.
   */
  public atLeastOneFacetIsActive() {
    return !_.isUndefined(
      _.find(this.attributes, (value, key: string) => {
        return key.match(/^f:/) && !Utils.arrayEqual(this.getDefault(key), value);
      })
    );
  }

  public set(attribute: string, value: any, options?: IModelSetOptions) {
    this.validate(attribute, value);
    super.set(attribute, value, options);
  }

  private validate(attribute: string, value: any) {
    if (attribute == QueryStateModel.attributesEnum.first) {
      Assert.isNumber(value);
      Assert.isLargerOrEqualsThan(0, value);
    }
  }
}

export function setState(model: Model, args: any[]): any {
  Assert.exists(model);

  if (args.length == 0 || args[0] == undefined) {
    // No args means return the model
    return model;
  } else if (args.length == 1 && Utils.isNonEmptyString(args[0])) {
    // One string arg means retrieve value from model
    return model.get(args[0]);
  } else if (_.isObject(args[0])) {
    // One dictionary means set multiple values
    let toSet = args[0];
    let options = _.extend(<IModelSetOptions>{ customAttribute: true }, <IModelSetOptions>args[1]);
    return model.setMultiple(toSet, options);
  } else if (args.length > 1) {
    // Otherwise we're setting a value
    let name = <string>args[0];
    let value = args[1];
    let options = _.extend(<IModelSetOptions>{ customAttribute: true }, <IModelSetOptions>args[2]);
    Assert.isNonEmptyString(name);
    return model.set(name, value, options);
  }
}
