/* global IITC, L */

/**
 * ### Filters API
 *
 * Filters API is a mechanism to hide intel entities using their properties (faction,
 * health, timestamp...). It provides two level APIs: a set of named filters that
 * apply globally (any entity matching one of the filters will be hidden), and low
 * level API to test an entity against a filter for generic purpose.
 * This comes with a Leaflet layer system following the old layer system, the filter
 * is disabled when the layer is added to the map and is enabled when removed.
 *
 * A filter applies to a combinaison of portal/link/field and is described by
 *  - data properties that must (all) match
 *  - or a predicate for complex filter
 *
 *   `{ portal: true, link: true, data: { team: 'E' }}`
 *       filters any ENL portal/link
 *
 *   `[{ link: true, data: { oGuid: "some guid" }}, { link: true, data: { dGuid: "some guid" }}]`
 *       filters any links on portal with guid "some guid"
 *
 *   `{ field: true, pred: function (f) { return f.options.timestamp < Date.parse('2021-10-31'); } }`
 *       filters any fields made before Halloween 2021
 *
 * Data properties can be specified as value, or as a complex expression (required
 * for array data properties). A complex expression is a 2-array, first element is
 * an operator, second is the argument of the operator for the property.
 * The operators are:
 *  - `['eq', value]` : this is equivalent to type directly `value`
 *  - `['not', ]`
 *  - `['or', [exp1, exp2,...]]`: the expression matches if one of the exp1.. matches
 *  - `['and', [exp1, exp2...]]`: matches if all exp1 matches (useful for array
 *   properties)
 *  - `['some', exp]`: when the property is an array, matches if one of the element
 *   matches `exp`
 *  - `['every', exp]`: all elements must match `exp`
 *  - `['<', number]`: for number comparison (and <= > >=)
 *
 * Examples:
 *
 *   `{ portal: true, data:  ['not', { history: { scoutControlled: false }, ornaments:
 *   ['some', 'sc5_p'] }] }`
 *       filters all portals but the one never scout controlled that have a scout
 *       volatile ornament
 *
 *   `{ portal: true, data: ['not', { resonators: ['every', { owner: 'some agent' } ] } ] }`
 *       filters all portals that have resonators not owned from 'some agent'
 *       (note: that would need to load portal details)
 *
 *   `{ portal: true, data: { level: ['or', [1,4,5]], health: ['>', 85] } }`
 *       filters all portals with level 1,4 or 5 and health over 85
 *
 *   `{ portal: true, link: true, field: true, options: { timestamp: ['<',
 *   Date.now() - 3600000] } }`
 *       filters all entities with no change since 1 hour (from the creation of
 *       the filter)
 *
 * @memberof IITC
 * @namespace filters
 */

IITC.filters = {};
/**
 * @type {Object.<string, IITC.filters.FilterDesc>}
 */
IITC.filters._filters = {};

/**
 * @memberof IITC.filters
 * @callback FilterPredicate
 * @param {Object} ent - IITC entity
 * @returns {boolean}
 */

/**
 * @memberof IITC.filters
 * @typedef FilterDesc
 * @type {object}
 * @property {boolean} filterDesc.portal         apply to portal
 * @property {boolean} filterDesc.link           apply to link
 * @property {boolean} filterDesc.field          apply to field
 * @property {object} [filterDesc.data]          entity data properties that must match
 * @property {object} [filterDesc.options]       entity options that must match
 * @property {IITC.filters.FilterPredicate} [filterDesc.pred] predicate on the entity
 */

/**
 * Sets or updates a filter with a given name. If a filter with the same name already exists, it is overwritten.
 *
 * @param {string} name                              filter name
 * @param {IITC.filters.FilterDesc | IITC.filters.FilterDesc[]} filterDesc     filter description (OR)
 */
IITC.filters.set = function (name, filterDesc) {
  IITC.filters._filters[name] = filterDesc;
};

/**
 * Checks if a filter with the specified name exists.
 *
 * @param {string} name - The name of the filter to check.
 * @returns {boolean} True if the filter exists, false otherwise.
 */
IITC.filters.has = function (name) {
  return name in IITC.filters._filters;
};

/**
 * Removes a filter with the specified name.
 *
 * @param {string} name - The name of the filter to be removed.
 * @returns {boolean} True if the filter was successfully deleted, false otherwise.
 */
IITC.filters.remove = function (name) {
  return delete IITC.filters._filters[name];
};

function compareValue(constraint, value) {
  if (constraint instanceof Array) return false;
  // array must be handled by "some" or "every"
  if (value instanceof Array) return false;
  if (constraint instanceof Object) {
    if (!(value instanceof Object)) return false;
    // implicit AND on object properties
    for (const prop in constraint) {
      if (!genericCompare(constraint[prop], value[prop])) {
        return false;
      }
    }
    return true;
  }
  return constraint === value;
}

function compareNumber(constraint, value) {
  if (typeof value !== 'number') return false;
  if (typeof constraint[1] !== 'number') return false;
  const v = constraint[1];
  switch (constraint[0]) {
    case '==':
      return value === v;
    case '<':
      return value < v;
    case '<=':
      return value <= v;
    case '>':
      return value > v;
    case '>=':
      return value >= v;
  }
  return false;
}

function genericCompare(constraint, object) {
  if (constraint instanceof Array) {
    if (constraint.length !== 2) return false;
    const [op, args] = constraint;
    switch (op) {
      case 'eq':
        return compareValue(args, object);
      case 'or':
        if (args instanceof Array) {
          for (const arg of args) {
            if (genericCompare(arg, object)) {
              return true;
            }
          }
        }
        return false;
      case 'and':
        if (args instanceof Array) {
          for (const arg of args) {
            if (!genericCompare(arg, object)) {
              return false;
            }
          }
        }
        return true;
      case 'some':
        if (object instanceof Array) {
          for (const obj of object) {
            if (genericCompare(args, obj)) {
              return true;
            }
          }
        }
        return false;
      case 'every':
        if (object instanceof Array) {
          for (const obj of object) {
            if (!genericCompare(args, obj)) {
              return false;
            }
          }
        }
        return true;
      case 'not':
        return !genericCompare(args, object);
      case '==':
      case '<':
      case '<=':
      case '>':
      case '>=':
        return compareNumber(constraint, object);
      default:
      // unknown op
    }
    return false;
  }
  return compareValue(constraint, object);
}

/**
 * Tests whether a given entity matches a specified filter.
 *
 * @param {"portal"|"link"|"field"} type Type of the entity
 * @param {object} entity Portal/link/field to test
 * @param {IITC.filters.FilterDesc} filter Filter
 * @returns {boolean} `true` if the the `entity` of type `type` matches the `filter`
 */
IITC.filters.testFilter = function (type, entity, filter) {
  // type must match
  if (!filter[type]) return false;
  // use predicate if available
  if (typeof filter.pred === 'function') return filter.pred(entity);
  // if doesn't match data constraint
  if (filter.data && !genericCompare(filter.data, entity.options.data)) return false;
  // if doesn't match options
  if (filter.options && !genericCompare(filter.options, entity.options)) {
    return false;
  }
  // else it matches
  return true;
};

function arrayFilter(type, entity, filters) {
  if (!Array.isArray(filters)) filters = [filters];
  filters = filters.flat();
  for (let i = 0; i < filters.length; i++) {
    if (IITC.filters.testFilter(type, entity, filters[i])) {
      return true;
    }
  }
  return false;
}

/**
 * Tests whether a given portal matches any of the currently active filters.
 *
 * @param {object} portal Portal to test
 * @returns {boolean} `true` if the the portal matches one of the filters
 */
IITC.filters.filterPortal = function (portal) {
  return arrayFilter('portal', portal, Object.values(IITC.filters._filters));
};

/**
 * Tests whether a given link matches any of the currently active filters.
 *
 * @param {object} link Link to test
 * @returns {boolean} `true` if the the link matches one of the filters
 */
IITC.filters.filterLink = function (link) {
  return arrayFilter('link', link, Object.values(IITC.filters._filters));
};

/**
 * Tests whether a given field matches any of the currently active filters.
 *
 * @param {object} field Field to test
 * @returns {boolean} `true` if the the field matches one of the filters
 */
IITC.filters.filterField = function (field) {
  return arrayFilter('field', field, Object.values(IITC.filters._filters));
};

/**
 * Applies all existing filters to the entities (portals, links, and fields) on the map.
 * Entities that match any of the active filters are removed from the map; others are added or remain on the map.
 */
IITC.filters.filterEntities = function () {
  for (const guid in window.portals) {
    const p = window.portals[guid];
    if (IITC.filters.filterPortal(p)) p.remove();
    else p.addTo(window.map);
  }
  for (const guid in window.links) {
    const link = window.links[guid];
    if (IITC.filters.filterLink(link)) link.remove();
    else link.addTo(window.map);
  }
  for (const guid in window.fields) {
    const field = window.fields[guid];
    if (IITC.filters.filterField(field)) field.remove();
    else field.addTo(window.map);
  }
};

/**
 * @memberof IITC.filters
 * @class FilterLayer
 * @description Layer abstraction to control with the layer chooser a filter.
 *              The filter is disabled on layer add, and enabled on layer remove.
 * @extends L.Layer
 * @param {Object} options - Configuration options for the filter layer
 * @param {string} options.name - The name of the filter
 * @param {IITC.filters.FilterDesc} options.filter - The filter description
 */
IITC.filters.FilterLayer = L.Layer.extend({
  options: {
    name: null,
    filter: {},
  },

  initialize: function (options) {
    L.setOptions(this, options);
    IITC.filters.set(this.options.name, this.options.filter);
  },

  onAdd: function () {
    IITC.filters.remove(this.options.name);
    IITC.filters.filterEntities();
  },

  onRemove: function () {
    IITC.filters.set(this.options.name, this.options.filter);
    IITC.filters.filterEntities();
  },
});