'use strict';

/* global L, log -- eslint */

/**
 * Represents a control for selecting layers on the map. It extends the Leaflet's L.Control.Layers class.
 * This control not only manages layer visibility but also provides persistence of layer display states between sessions.
 * The class has been enhanced with additional options and methods for more flexible layer management.
 *
 * @memberof L
 * @class LayerChooser
 * @extends L.Control.Layers
 */
var LayerChooser = L.Control.Layers.extend({
  options: {
    /**
     * @property {Boolean} sortLayers=true - Ensures stable sort order (based on initial), while still providing
     *                                       ability to enforce specific order with `addBaseLayer`/`addOverlay`
     *                                       `sortPriority` option.
     */
    sortLayers: true,

    /**
     * @property {Function} sortFunction - A compare function that will be used for sorting the layers,
     *                                     when `sortLayers` is `true`. The function receives objects with
     *                                     the layer's data.
     * @see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
     */
    sortFunction: function (A, B) {
      var a = A.sortPriority;
      var b = B.sortPriority;
      return a < b ? -1 : b < a ? 1 : 0;
    },
  },

  /**
   * Initializes a new instance of the LayerChooser control.
   *
   * @memberof LayerChooser
   * @method
   * @param {L.Layer[]} baseLayers - Array of base layers to include in the chooser.
   * @param {L.Layer[]} overlays - Array of overlay layers to include in the chooser.
   * @param {Object} [options] - Additional options for the LayerChooser control.
   */
  initialize: function (baseLayers, overlays, options) {
    this._overlayStatus = {};
    var layersJSON = localStorage['ingress.intelmap.layergroupdisplayed'];
    if (layersJSON) {
      try {
        this._overlayStatus = JSON.parse(layersJSON);
      } catch (e) {
        log.error(e);
      }
    }
    this._mapToAdd = options && options.map;
    this.lastBaseLayerName = localStorage['iitc-base-map'];
    this._lastPriority = -1000; // initial layers get priority <0
    L.Control.Layers.prototype.initialize.apply(this, arguments);
    this._lastPriority = 0; // any following gets >0
  },

  _addLayer: function (layer, name, overlay, options) {
    options = options || {};
    // _chooser property stores layerChooser data after layer removal
    // (in case if it's meant to be re-added)
    var data = layer._chooser;
    if (!data) {
      data = {
        layer: layer,
        // name should be unique, otherwise behavior of other methods is undefined
        // (typically: first found will be taken)
        name: name,
        // label: name,
        overlay: overlay,
        persistent: 'persistent' in options ? options.persistent : true,
      };
    } else {
      delete layer._chooser;
    }
    // provide stable sort order
    if ('sortPriority' in options) {
      data.sortPriority = options.sortPriority;
    } else if (!('sortPriority' in data)) {
      this._lastPriority = this._lastPriority + 10;
      data.sortPriority = this._lastPriority;
    }
    // *** adapted from L.Control.Layers.prototype._addLayer.call(this, layer, name, overlay);
    if (this._map) {
      layer.on('add remove', this._onLayerChange, this);
    }

    this._layers.push(data);

    if (this.options.sortLayers) {
      this._layers.sort(this.options.sortFunction);
    }

    if (this.options.autoZIndex && layer.setZIndex) {
      this._lastZIndex++;
      layer.setZIndex(this._lastZIndex);
    }

    this._expandIfNotCollapsed();
    // ***

    if (data.overlay) {
      data.default = 'default' in options ? options.default : true;
    }
    var map = this._map || this._mapToAdd;
    if (!data.persistent) {
      if (!data.overlay) {
        return;
      }
      if ('enable' in options ? options.enable : data.default) {
        layer.addTo(map);
      }
      return;
    }
    if (overlay) {
      data.statusTracking = function (e) {
        this._storeOverlayState(data.name, e.type === 'add');
      };
      layer.on('add remove', data.statusTracking, this);
      if ('enable' in options) {
        // do as explicitly specified
        map[options.enable ? 'addLayer' : 'removeLayer'](layer);
      } else if (layer._map) {
        // already on map, only store state
        this._storeOverlayState(data.name, true);
      } else {
        // restore at recorded state
        if (this._isOverlayDisplayed(data.name, data.default)) {
          layer.addTo(map);
        }
      }
    } else {
      data.statusTracking = function () {
        localStorage['iitc-base-map'] = data.name;
      };
      layer.on('add', data.statusTracking);
    }
  },

  _addItem: function (obj) {
    var labelEl = L.Control.Layers.prototype._addItem.call(this, {
      layer: obj.layer,
      overlay: obj.overlay,
      name: obj.label || obj.name,
    });
    obj.labelEl = labelEl;
    // obj.inputEl = this._layerControlInputs[this._layerControlInputs.length-1];
    return labelEl;
  },

  /**
   * Adds a base layer (radio button entry) with the given name to the control.
   *
   * @memberof LayerChooser
   * @param {L.Layer} layer - The layer to be added.
   * @param {String} name - The name of the layer.
   * @param {Object} [options] - Additional options for the layer entry.
   * @param {Boolean} [options.persistent=true] - When set to `false`, the base layer's status is not tracked.
   * @param {Number} [options.sortPriority] - Enforces a specific order in the control. Lower value means
   *                                          higher position in the list. If not specified, the value
   *                                          will be assigned implicitly in an increasing manner.
   * @returns {LayerChooser} Returns the `LayerChooser` instance for chaining.
   */
  addBaseLayer: function (layer, name, options) {
    this._addLayer(layer, name, false, options);
    return this._map ? this._update() : this;
  },

  /**
   * Adds an overlay (checkbox entry) with the given name to the control.
   *
   * @memberof LayerChooser
   * @param {L.Layer} layer - The overlay layer to be added.
   * @param {String} name - The name of the overlay.
   * @param {Object} [options] - Additional options for the overlay entry.
   * @param {Boolean} [options.persistent=true] - When `true` (or not specified), the overlay is added to the map
   *                                              if its last state was active. If no previous state is recorded,
   *                                              the value specified in the `default` option is used.
   *                                              When `false`, the overlay status is not tracked,
   *                                              but the `default` option is still honored.
   * @param {Boolean} [options.default=true] - The default state of the overlay, used only when no record
   *                                           of the previous state is found.
   * @param {Boolean} [options.enable] - If set, enforces the specified state, ignoring any previously saved state.
   * @returns {LayerChooser} Returns the `LayerChooser` instance for chaining.
   */
  addOverlay: function (layer, name, options) {
    this._addLayer(layer, name, true, options);
    return this._map ? this._update() : this;
  },

  /**
   * Removes the given layer from the control.
   *
   * @memberof LayerChooser
   * @param {L.Layer|String} layer - The layer to be removed, either as a Leaflet layer object or its name.
   * @param {Object} [options] - Additional options, including `keepOnMap` to keep the layer on the map.
   * @returns {LayerChooser} Returns the `LayerChooser` instance for chaining.
   */
  removeLayer: function (layer, options) {
    layer = this.getLayer(layer);
    var data = this.layerInfo(layer);
    if (data) {
      options = options || {};
      if (data.statusTracking) {
        data.layer.off('add remove', data.statusTracking, this);
        delete data.statusTracking;
      }
      L.Control.Layers.prototype.removeLayer.apply(this, arguments);
      if (this._map && !options.keepOnMap) {
        window.map.removeLayer(data.layer);
      }
      delete data.labelEl;
      // delete data.inputEl;
      layer._chooser = data;
    } else {
      log.warn('Layer not found: ', layer);
    }
    return this;
  },

  _storeOverlayState: function (name, isDisplayed) {
    this._overlayStatus[name] = isDisplayed;
    localStorage['ingress.intelmap.layergroupdisplayed'] = JSON.stringify(this._overlayStatus);
  },

  _isOverlayDisplayed: function (name, defaultState) {
    if (name in this._overlayStatus) {
      return this._overlayStatus[name];
    }
    return defaultState;
  },

  __byName: function (data) {
    var name = this.toString();
    return data.name === name || data.label === name;
  },

  __byLayer: function (data) {
    return data.layer === this;
  },

  __byLabelEl: function (data) {
    return data.labelEl === this;
  },

  // @method layerInfo(name: String|Layer): Layer
  // Returns layer info by it's name in the control, or by layer object itself,
  // or label html element.
  // Info is internal data object with following properties:
  // `layer`, `name`, `label`, `overlay`, `sortPriority`, `persistent`, `default`,
  // `labelEl`, `inputEl`, `statusTracking`.
  /**
   * Retrieves layer info by its name in the control, or by the layer object itself, or its label HTML element.
   *
   * @memberof LayerChooser
   * @param {String|L.Layer|HTMLElement} layer - The name, layer object, or label element of the layer.
   * @returns {Object} Layer info object with following properties: `layer`, `name`, `label`, `overlay`, `sortPriority`,
   *                   `persistent`, `default`, `labelEl`, `inputEl`, `statusTracking`.
   */
  layerInfo: function (layer) {
    var fn = layer instanceof L.Layer ? this.__byLayer : layer instanceof HTMLElement ? this.__byLabelEl : this.__byName;
    return this._layers.find(fn, layer);
  },

  /**
   * Returns the Leaflet layer object based on its name in the control, or the layer object itself,
   * or its label HTML element. The latter can be used to ensure the layer is in layerChooser.
   *
   * @memberof LayerChooser
   * @param {String|L.Layer|HTMLElement} layer - The name, layer object, or label element of the layer.
   * @returns {L.Layer} The corresponding Leaflet layer object.
   */
  getLayer: function (layer) {
    var data = this.layerInfo(layer);
    return data && data.layer;
  },

  /**
   * Shows or hides a specified basemap or overlay layer. The layer can be specified by its ID, name, or layer object.
   * If the display parameter is not provided, the layer will be shown by default.
   * When showing a base layer, it ensures that no other base layers are displayed at the same time.
   *
   * @memberof LayerChooser
   * @param {L.Layer|String|Number} layer - The layer to show or hide. This can be a Leaflet layer object,
   *                                        a layer name, or a layer ID.
   * @param {Boolean} [display=true] - Pass `false` to hide the layer, or `true`/omit to show it.
   * @returns {LayerChooser} Returns the `LayerChooser` instance for chaining.
   */
  showLayer: function (layer, display) {
    var data = this._layers[layer]; // layer is index, private use only
    if (!data) {
      data = this.layerInfo(layer);
      if (!data) {
        log.warn('Layer not found: ', layer);
        return this;
      }
    }
    var map = this._map;
    if (display || arguments.length === 1) {
      if (!map.hasLayer(data.layer)) {
        if (!data.overlay) {
          // if it's a base layer, remove any others
          this._layers.forEach(function (el) {
            if (!el.overlay && el.layer !== data.layer) {
              map.removeLayer(el.layer);
            }
          });
        }
        map.addLayer(data.layer);
      }
    } else {
      map.removeLayer(data.layer);
    }
    return this;
  },

  /**
   * Sets the label of a layer in the control.
   *
   * @memberof LayerChooser
   * @param {String|L.Layer} layer - The name or layer object.
   * @param {String} [label] - The label text (HTML allowed) to set. Resets to original name if not provided.
   * @returns {LayerChooser} Returns the `LayerChooser` instance for chaining.
   */
  setLabel: function (layer, label) {
    var data = this.layerInfo(layer);
    if (!data) {
      log.warn('Layer not found: ', layer);
      return this;
    }
    data.label = label;
    var nameEl = data.labelEl.querySelector('span');
    nameEl.innerHTML = ' ' + label;
    return this;
  },

  _onLongClick: function (data, originalEvent) {
    var defaultPrevented;

    // @miniclass LayersControlInteractionEvent (LayerChooser)
    // @inherits Event
    // @property layer: L.Layer
    // The layer that was interacted in LayerChooser control.
    // @property control: LayerChooser
    // LayerChooser control instance (just handy shortcut for window.layerChooser).
    // @property data: Object
    // Internal data object TODO
    // @property originalEvent: DOMEvent
    // The original mouse/jQuery event that triggered this Leaflet event.
    // @method preventDefault: Function
    // Method to prevent default action of event (like overlays toggling), otherwise handled by layerChooser.
    var obj = {
      control: this,
      data: data,
      originalEvent: originalEvent || { type: 'taphold' },
      preventDefault: function () {
        defaultPrevented = true;
        this.defaultPrevented = true;
      },
    };

    // @namespace Layer
    // @section Layers control interaction events
    // Fired when the overlay's label is long-clicked in the layers control.

    // @section Layers control interaction events
    // @event longclick: LayersControlInteractionEvent
    // Fired on layer
    data.layer.fire('longclick', obj);
    if (!defaultPrevented) {
      this._toggleOverlay(data);
    }
    // @namespace LayerChooser
  },

  // adds listeners to the overlays list to make inputs toggleable.
  _initLayout: function () {
    L.Control.Layers.prototype._initLayout.call(this);
    $(this._overlaysList).on(
      'click taphold',
      'label',
      function (e) {
        if (!(e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.type === 'taphold')) {
          return;
        }
        // e.preventDefault(); // seems no effect
        var labelEl = e.target.closest('label');
        this._onLongClick(this.layerInfo(labelEl), e);
      }.bind(this)
    );
  },

  _filterOverlays: function (data) {
    return data.overlay && ['DEBUG Data Tiles', 'Resistance', 'Enlightened'].indexOf(data.name) === -1;
  },

  // Hides all the control's overlays except given one,
  // or restores all, if it was the only one displayed (or none was displayed).
  _toggleOverlay: function (data) {
    if (!data || !data.overlay) {
      log.warn('Overlay not found: ', data);
      return;
    }
    var map = this._map;

    var isChecked = map.hasLayer(data.layer);
    var checked = 0;
    var overlays = this._layers.filter(this._filterOverlays);
    overlays.forEach(function (el) {
      if (map.hasLayer(el.layer)) {
        checked++;
      }
    });

    if (checked === 0 || (isChecked && checked === 1)) {
      // if nothing is selected, or specified overlay is exclusive,
      // assume all boxes should be checked again
      overlays.forEach(function (el) {
        if (el.default) {
          map.addLayer(el.layer);
        }
      });
    } else {
      // uncheck all, check specified
      overlays.forEach(function (el) {
        if (el.layer === data.layer) {
          map.addLayer(el.layer);
        } else {
          map.removeLayer(el.layer);
        }
      });
    }
  },

  _stripHtmlTags: function (str) {
    return str.replace(/(<([^>]+)>)/gi, ''); // https://css-tricks.com/snippets/javascript/strip-html-tags-in-javascript/
  },

  /**
   * Retrieves the current state of base and overlay layers managed by this control.
   * This method is deprecated and should be used with caution.
   *
   * The method returns an object with two properties: 'baseLayers' and 'overlayLayers'.
   * Each array contains objects representing the respective layers with properties: 'layerId', 'name', and 'active'.
   * 'layerId' is an internal identifier for the layer, 'name' is the layer's name, and 'active' is a boolean indicating
   * if the layer is currently active on the map.
   *
   * @memberof LayerChooser
   * @deprecated
   * @returns {{overlayLayers: Array, baseLayers: Array}} An object containing arrays of base and overlay layers.
   */
  getLayers: function () {
    var baseLayers = [];
    var overlayLayers = [];
    this._layers.forEach(function (data, idx) {
      (data.overlay ? overlayLayers : baseLayers).push({
        layerId: idx,
        name: this._stripHtmlTags(data.label || data.name), // IITCm does not support html in layers labels
        active: this._map.hasLayer(data.layer),
      });
    }, this);

    return {
      baseLayers: baseLayers,
      overlayLayers: overlayLayers,
    };
  },
});

window.LayerChooser = LayerChooser;

// contains current status(on/off) of overlay layerGroups.
// !!deprecated: use `map.hasLayer` instead (https://leafletjs.com/reference.html#map-haslayer)
window.overlayStatus = {}; // to be set in constructor

// Reads recorded layerGroup status (as it may not be added to map yet),
// return `defaultDisplay` if no record found.
// !!deprecated: for most use cases prefer `getLayer()` method
// or `map.hasLayer` (https://leafletjs.com/reference.html#map-haslayer)
// window.isLayerGroupDisplayed = function (name, defaultDisplay) { // ...
window.isLayerGroupDisplayed = L.Util.falseFn; // to be set in constructor

LayerChooser.addInitHook(function () {
  window.overlayStatus = this._overlayStatus;
  window.isLayerGroupDisplayed = this._isOverlayDisplayed.bind(this);
});

// !!deprecated: use `layerChooser.addOverlay` directly
window.addLayerGroup = function (name, layerGroup, defaultDisplay) {
  var options = { default: defaultDisplay };
  if (arguments.length < 3) {
    options = undefined;
  }
  window.layerChooser.addOverlay(layerGroup, name, options);
};

// !!deprecated: use `layerChooser.removeLayer` directly
// our method differs from inherited (https://leafletjs.com/reference.html#control-layers-removelayer),
// as (by default) layer is removed from the map as well, see description for more details.
window.removeLayerGroup = function (layerGroup) {
  window.layerChooser.removeLayer(layerGroup);
};