/* datatable: advanced search */

(function(factory) {
  "use strict";
  require(["AbilianNS", "jquery", "jquery.dataTables"], factory);
})(function(Abilian, $) {
  "use strict";

  function defaultDatatableConfig() {
    if (!Abilian.DEBUG) {
      /* deactivate datatable issuing 'alert' on any error in production.
       * It confuses users. */
      $.fn.dataTable.ext.sErrMode = "";
    }
  }

  Abilian.fn.onAppInit(defaultDatatableConfig);

  /* create new filter instance */
  function instantiateFilter(filterType, args) {
    var FilterClass = AdvancedSearchFilters.oFilters[filterType];
    var instance = Object.create(FilterClass.prototype);
    FilterClass.apply(instance, args);
    return instance;
  }

  /*
   * Helper to check equality for filter values
   */
  function hasValueSet(filter) {
    var unset = filter.unsetValue;
    var val = filter.save();

    if (filter.hasValueSet !== undefined) {
      return filter.hasValueSet();
    }

    if (!Array.isArray(val)) {
      return true;
    }

    // ensure unset is an array
    if (!Array.isArray(unset)) {
      unset = [unset];
    }

    var length = unset.length;
    if (unset.length != val.length) {
      return true;
    }

    for (var key = 0; key < length; key++) {
      if (unset[key] !== val[key]) {
        return true;
      }
    }
    return false;
  }

  // helper function to unescape html
  var tagsToReplace = {
    "&amp;": "&",
    "&lt;": "<",
    "&gt;": ">",
  };

  function replaceTag(tag) {
    return tagsToReplace[tag] || tag;
  }

  function safe_tags_replace(element) {
    var output = element.text.replace(/&amp;/g, replaceTag);
    output = output.replace(/&lt;/g, replaceTag);
    output = output.replace(/&gt;/g, replaceTag);
    return output;
  }

  /*
   * Create an event handler for a filter 'close' button
   */
  function setupCloseButtonHandler(advInstance, instance) {
    function closeFilter(e) {
      e.preventDefault();
      advInstance.unsetFilter(instance.name);
      advInstance.removeFilter(instance.name);
      instance.$container.trigger("redraw.DT");
    }

    return closeFilter;
  }

  /**
   * Additional search criterias for DataTable with Ajax source
   *
   * @class AdvancedSearchFilters
   * @constructor
   * @param {object} oDTSettings Settings for the target DataTable.
   */
  var AdvancedSearchFilters = function(oDTSettings) {
    var self = this;
    self.$Container = null;

    if (
      !(
        oDTSettings.oInit.bFilter &&
        oDTSettings.oInit.bServerSide &&
        oDTSettings.oInit.sAjaxSource &&
        "aoAdvancedSearchFilters" in oDTSettings.oInit &&
        oDTSettings.oInit.aoAdvancedSearchFilters.length > 0
      )
    ) {
      return;
    }

    self.aFilters = [];
    self.oFilters = {};
    self.oActiveFilters = {};

    /* filters container */
    self.$Container = $('<div class="advanced-search-filters"></div>');
    var filterSelectContainer = $('<div class="row" />');
    var filterSelect = $("<select />");
    var sAddAdvancedFilter =
      (oDTSettings.oLanguage.sAddAdvancedFilter || "Add a filter") + "...";
    var filtersContainer = $("<div />").attr({ class: "form-horizontal" });

    filterSelectContainer.append(filterSelect);
    filterSelect.append($('<option value=""></option>'));

    self.$filtersContainer = filtersContainer;
    self.$filterSelect = filterSelect;

    var aoasf_len = oDTSettings.oInit.aoAdvancedSearchFilters.length;
    for (var i = 0; i < aoasf_len; i++) {
      var $criterionContainer = $("<div></div>").attr({
        class: "criterion form-group",
      });
      var $labelContainer = $("<div></div>").attr({
        class: "col-xs-12 col-sm-3 control-label",
      });
      var $filterContainer = $("<div></div>").attr({
        class: "col-xs-12 col-sm-9",
      });
      var filter = oDTSettings.oInit.aoAdvancedSearchFilters[i];
      var args = [].concat([filter.name, filter.label], filter.args);
      var instance = instantiateFilter(filter.type, args);
      var defaultValue = filter.defaultValue;
      var $option = $("<option>" + filter.label + "</option>").attr({
        value: filter.name,
      });
      var $closeButton = $("<button />")
        .attr({ class: "close" })
        .append($("<span />").attr({ class: "glyphicon glyphicon-minus" }))
        .on("click", setupCloseButtonHandler(self, instance));

      instance.type = filter.type;
      instance.unsetValue = filter.unset;

      if (defaultValue !== undefined && !Array.isArray(defaultValue)) {
        defaultValue = [defaultValue];
      }
      instance.defaultValue = defaultValue;

      instance.$container = $criterionContainer;
      /* setup criterion container: label, inputs */
      $criterionContainer.hide();
      $criterionContainer.append($labelContainer, $filterContainer);
      $labelContainer.append($closeButton);

      if (instance.label !== "") {
        $labelContainer.append(
          $("<label>").append($("<strong />").text(instance.label))
        );
      }

      $filterContainer.append(instance.getElements());

      self.aFilters.push(instance);
      self.oFilters[filter.name] = instance;
      filterSelect.append($option);
      filtersContainer.append($criterionContainer);
    }

    filterSelect.select2({
      containerCssClass: "col-xs-4 col-md-3",
      placeholder: sAddAdvancedFilter,
    });
    filterSelect.on("change", function() {
      self.addFilter(this.value);
      $(this)
        .data("select2")
        .clear();
    });
    self.$Container.append(filterSelectContainer, filtersContainer);

    /* datatables events callbacks */
    oDTSettings.oInstance.bind(
      "serverParams",
      { instance: self },
      AdvancedSearchFilters.serverParamsCallBack
    );
    oDTSettings.oInstance.bind(
      "stateSaveParams",
      { instance: self },
      AdvancedSearchFilters.stateSaveParams
    );
    oDTSettings.oInstance.bind(
      "stateLoaded",
      { instance: self },
      AdvancedSearchFilters.stateLoaded
    );

    /* when we bind 'stateLoaded' here, state has already been loaded, in DT
     init.  We need to call the handler
     */
    if (oDTSettings.oLoadedState) {
      AdvancedSearchFilters.stateLoaded(
        { data: { instance: self } },
        oDTSettings,
        oDTSettings.oLoadedState
      );
    }
    self.$Container.on("redraw.DT", function() {
      oDTSettings.oInstance.fnDraw();
    });
    self.$Container.on("change.DT", "input, select", function() {
      oDTSettings.oInstance.fnDraw();
    });
  };

  /**
   * Filters registry
   */
  AdvancedSearchFilters.oFilters = {};

  /**
   * Get the container node of the advanced search filters
   *
   * @method
   * @return {Node} The container node.
   */
  AdvancedSearchFilters.prototype.getContainer = function() {
    return this.$Container && this.$Container.get(0);
  };

  /**
   * Add a new active filter
   *
   * @method
   */
  AdvancedSearchFilters.prototype.addFilter = function(filterName) {
    var instance = this.oFilters[filterName];
    if (instance === undefined) {
      return;
    }

    if (this.oActiveFilters[filterName] !== undefined) {
      // filter already active
      return;
    }

    this.oActiveFilters[filterName] = instance;

    /* install default value if possible and necessary: addFilter may be
     * called from stateLoaded() with a value already set by load
     * function */
    if (!hasValueSet(instance) && instance.defaultValue !== undefined) {
      instance.load(instance.defaultValue);
    }

    instance.$container.show();
    this.$filterSelect
      .find('option[value="' + filterName + '"]')
      .prop("disabled", true);
  };

  /**
   * Clear a filter
   */
  AdvancedSearchFilters.prototype.unsetFilter = function(filterName) {
    var instance = this.oFilters[filterName];
    if (instance === undefined) {
      return;
    }

    var unset = instance.unsetValue;
    if (!Array.isArray(unset)) {
      unset = [unset];
    }
    instance.load(unset);
  };

  /**
   * Remove a filter
   *
   * @method
   */
  AdvancedSearchFilters.prototype.removeFilter = function(filterName) {
    var instance = this.oFilters[filterName];
    if (instance === undefined) {
      return;
    }

    if (this.oActiveFilters[filterName] === undefined) {
      // not in active filters
      return;
    }

    delete this.oActiveFilters[filterName];
    instance.$container.hide();
    this.$filterSelect
      .find('option[value="' + filterName + '"]')
      .prop("disabled", null);
  };

  /**
   * Callback to fill server params before ajax request
   */
  AdvancedSearchFilters.serverParamsCallBack = function(event, aoData) {
    var self = event.data.instance;

    function pushFilterValue(filterName) {
      var f = self.oFilters[filterName];
      var vals = f.val();

      if (!(vals instanceof Array)) {
        vals = [vals];
      }
      $(vals).each(function() {
        aoData.push({ name: f.name, value: this });
      });
    }

    Object.keys(self.oActiveFilters)
      .sort()
      .forEach(pushFilterValue);
  };

  /**
   * Callback to save filters state
   */
  AdvancedSearchFilters.stateSaveParams = function(event, oSettings, oData) {
    var self = event.data.instance;
    oData.oAdvancedSearchFilters = {};
    Object.keys(self.oActiveFilters).forEach(function(filterName, idx) {
      var filter = self.oFilters[filterName];
      if (filter.save === undefined) {
        return;
      }
      this[filter.name] = filter.save();
    }, oData.oAdvancedSearchFilters);
    return true;
  };

  /**
   * Callback to restore filters state
   */
  AdvancedSearchFilters.stateLoaded = function(event, oSettings, oData) {
    var self = event.data.instance;
    var params = oData.oAdvancedSearchFilters || {};

    self.aFilters.forEach(function(filter, idx) {
      if (filter.load === undefined || this[filter.name] === undefined) {
        return;
      }

      filter.load(this[filter.name]);
      var instance = self.oFilters[filter.name];
      if (hasValueSet(instance)) {
        self.addFilter(filter.name);
      }
    }, params);

    return true;
  };

  /* setup standard filters */
  AdvancedSearchFilters.oFilters.text = (function() {
    function TextFilter(name, label) {
      this.name = name;
      this.label = label;
      this.$input = $("<input />").attr({ type: "text", name: name });
    }

    TextFilter.prototype = {
      getElements: function() {
        return this.$input;
      },
      val: function() {
        return [this.$input.val()];
      },
      save: function() {
        return [this.$input.val()];
      },
      load: function(vals) {
        this.$input.val(vals[0]);
      },
    };
    return TextFilter;
  })();

  AdvancedSearchFilters.oFilters.radio = (function() {
    function RadioFilter(name, label) {
      var checked = false;
      var arg_len = arguments.length;

      this.name = name;
      this.label = label;
      this.$elements = $("<div></div>");

      for (var i = 2; i < arg_len; i++) {
        var arg = arguments[i];
        var id = name + "_" + i;
        var input = $('<input type="radio">').attr({
          id: id,
          name: name,
          value: arg.value,
        });

        if (!checked && arg.checked) {
          input.prop("checked", true);
          checked = true;
        }

        var $label = $("<label></label>")
          .attr({ class: "radio-inline", for: id })
          .append(input)
          .append(document.createTextNode(arg.label));

        this.$elements.append($label);
      }

      if (!checked) {
        this.$elements
          .children("input")
          .first()
          .prop("checked", true);
      }
    }

    RadioFilter.prototype = {
      getElements: function() {
        return this.$elements;
      },
      val: function() {
        return [this.$elements.find("input:checked").val()];
      },
      save: function() {
        return [this.$elements.find("input:checked").val()];
      },
      load: function(vals) {
        this.$elements.find("input").each(function() {
          this.checked = this.value == vals[0];
        });
      },
    };
    return RadioFilter;
  })();

  AdvancedSearchFilters.oFilters.checkbox = (function() {
    function CheckboxFilter(name, label) {
      var checked = false;
      var arg_len = arguments.length;

      this.name = name;
      this.label = label;
      this.$elements = $("<div></div>");

      for (var i = 2; i < arg_len; i++) {
        var arg = arguments[i];
        var id = name + "_" + i;
        var input = $('<input type="checkbox">').attr({
          id: id,
          name: name,
          value: arg.value,
        });

        if (!checked && arg.checked) {
          input.prop("checked", true);
          checked = true;
        }

        var $label = $("<label></label>")
          .attr({ class: "checkbox-inline", for: id })
          .append(input)
          .append(document.createTextNode(arg.label));

        this.$elements.append($label);
      }

      if (!checked) {
        this.$elements
          .children("input")
          .first()
          .prop("checked", true);
      }
    }

    function getVal(container) {
      return container
        .find("input:checked")
        .map(function() {
          return $(this).val();
        })
        .get();
    }

    CheckboxFilter.prototype = {
      getElements: function() {
        return this.$elements;
      },
      val: function() {
        return getVal(this.$elements);
      },
      save: function() {
        return getVal(this.$elements);
      },
      load: function(vals) {
        this.$elements.find("input").each(function() {
          this.checked = vals.indexOf(this.value) !== -1;
        });
      },
    };
    return CheckboxFilter;
  })();

  AdvancedSearchFilters.oFilters.select = (function() {
    function SelectFilter(name, label, options, multiple) {
      this.name = name;
      this.label = label;
      this.multiple = multiple;
      multiple = multiple || false;

      var s2_options = [];
      var $elements = $("<div>");
      var $select = $("<input />").attr({
        id: name,
        name: name,
        type: "hidden",
      });

      this.$elements = $elements;
      this.$select = $select;
      $elements.append($select);

      for (var i = 0; i < options.length; i++) {
        var opt = options[i];
        s2_options.push({ id: opt[0], text: opt[1] });
      }

      $select.select2({
        data: s2_options,
        placeholder: multiple ? "" : "...",
        multiple: multiple,
        allowClear: true,
        width: "20em",
        "max-width": "100%",
        containerCssClass: "form-control",
        containerCss: { "margin-left": "0.5em" },
        formatResult: safe_tags_replace,
        formatSelection: safe_tags_replace,
      });
    }

    function getVal() {
      /* jshint validthis: true */
      var val = this.$select.data("select2").val();
      if (!this.multiple && !val.length) {
        val = [];
      }
      return val;
    }

    SelectFilter.prototype = {
      getElements: function() {
        return this.$elements;
      },
      val: getVal,
      save: getVal,
      load: function(vals) {
        this.$select.data("select2").val(vals);
      },
    };
    return SelectFilter;
  })();

  AdvancedSearchFilters.oFilters.selectAjax = (function() {
    function SelectAjaxFilter(name, label, ajax_source, multiple) {
      this.name = name;
      this.label = label;
      this.multiple = multiple;
      multiple = multiple || false;

      var s2_options = [];
      var $elements = $("<div>");
      var $select = $("<input />").attr({
        id: name,
        name: name,
        type: "hidden",
      });

      this.$elements = $elements;
      this.$select = $select;
      $elements.append($select);

      $select.select2({
        data: s2_options,
        placeholder: multiple ? "" : "...",
        multiple: multiple,
        allowClear: true,
        width: "20em",
        "max-width": "100%",
        containerCssClass: "form-control",
        containerCss: { "margin-left": "0.5em" },
        minimumInputLength: 2,
        ajax: {
          url: ajax_source,
          dataType: "json",
          quietMillis: 200,
          data: function(term, page) {
            return { q: term };
          },
          results: function(data, page) {
            return { results: data.results, more: false };
          },
        },
      }); // select2
    }

    function getVal() {
      /* jshint validthis: true */
      var val = this.$select.data("select2").val();
      if (!this.multiple && !val.length) {
        val = [];
      }
      return val;
    }

    function saveVal() {
      /* jshint validthis: true */
      var data = this.$select.data("select2").data();

      if (data) {
        if (!this.multiple && !data.length) {
          data = [];
        }
      } else {
        data = null;
      }

      return data;
    }

    SelectAjaxFilter.prototype = {
      getElements: function() {
        return this.$elements;
      },
      val: getVal,
      save: saveVal,
      load: function(vals) {
        this.$select.data("select2").data(vals);
      },
    };
    return SelectAjaxFilter;
  })();

  AdvancedSearchFilters.oFilters["select-radio"] = (function() {
    function SelectRadioFilter(name, label, s2_args /*, radio_args, ... */) {
      /*
       a select box followed by 3 radios (boolean all/True/False)
       s2_args: contains the select2 data
       radio_args...: a radio is created for each param after s2_args
       */
      var checked = false;
      var argLen = arguments.length;
      this.name = name;
      this.label = label;
      this.$elements = $('<div class="form-inline">');
      this.multiple = s2_args["multiple"] || false;

      /* create the select */
      var selectId = name + "-select";
      var $select = $("<input />").attr({
        id: selectId,
        name: selectId,
        type: "hidden",
      });
      var s2Label = $("<label></label>")
        .attr({ class: "select-inline", for: selectId })
        .append($select);
      // .append(document.createTextNode(name));
      this.$select = $select;
      this.$elements.append(s2Label, $select);

      $select.select2({
        data: s2_args["select-data"],
        placeholder: s2_args["select-label"] || "",
        multiple: this.multiple,
        allowClear: true,
        width: "20em",
        "max-width": "100%",
        containerCssClass: "form-control",
        containerCss: { "margin-left": "0.5em" },
      });
      this.$elements.append("&nbsp;&nbsp;");

      /* create the radios */
      for (var i = 3; i < argLen; i++) {
        var arg = arguments[i];
        var id = name + "-radio" + "_" + i;
        var $input = $('<input type="radio">').attr({
          id: id,
          name: name + "-radio",
          value: arg.value,
        });

        if (!checked && arg.checked) {
          $input.prop("checked", true);
          checked = true;
        }

        var radioLabel = $("<label></label>")
          .attr({ class: "radio-inline", for: id })
          .append($input)
          .append(document.createTextNode(arg.label));

        this.$elements.append(radioLabel);
      }
    }

    function getVal() {
      /*
       get value to fill the  /GET : response with the attribute
       return: list(select:id, radio:value)
       */
      /* jshint validthis: true */
      var radioValue = this.$elements.find("input:checked").val();
      var select2Value = this.$select.select2("val");

      if (select2Value || radioValue !== "None") {
        return [select2Value, radioValue];
      }
      return [];
    }

    SelectRadioFilter.prototype = {
      getElements: function() {
        return this.$elements;
      },
      val: getVal,
      save: getVal,
      load: function(vals) {
        if (vals.length === 0) {
          return;
        }
        this.$elements
          .find("label input")
          .first()
          .prop("checked", true);
        this.$select.select2("val", vals[0]);
      },
    };

    return SelectRadioFilter;
  })();

  AdvancedSearchFilters.oFilters["checkbox-select"] = (function() {
    function CheckboxSelectFilter(name, label, args) {
      var self = this;
      this.name = name;
      this.label = label;
      this.$elements = $("<div>");
      this.$input = $('<input type="checkbox">').attr({
        id: name,
        name: name,
        value: name,
        checked: "checked",
      });

      var select_id = name + "-select";
      var $label = $("<label></label>")
        .attr({ class: "checkbox-inline", for: name })
        .append(this.$input)
        .text(args.label);

      this.$elements.append($label);
      this.$select = $("<input />").attr({
        id: select_id,
        name: select_id,
        type: "hidden",
      });
      this.$elements.append(this.$select);
      this.$select.select2({
        data: args["select-data"],
        placeholder: args["select-label"] || "",
        allowClear: true,
        width: "20em",
        "max-width": "100%",
        containerCssClass: "form-control",
        containerCss: { "margin-left": "0.5em" },
      });

      this.$input.on("change", function() {
        self.$select.select2("enable", this.checked);
      });
    }

    function getVal() {
      /* jshint validthis: true */
      if (this.$input.get(0).checked) {
        return [this.$select.select2("val") || this.$input.val()];
      }
      return [];
    }

    CheckboxSelectFilter.prototype = {
      getElements: function() {
        return this.$elements;
      },
      val: getVal,
      save: getVal,
      load: function(vals) {
        if (vals.length === 0) {
          return;
        }
        this.$input.get(0).setAttribute("checked", true);
        this.$select.select2("val", vals[0]);
      },
    };
    return CheckboxSelectFilter;
  })();

  AdvancedSearchFilters.oFilters.optional_criterions = (function() {
    /* legacy filter. Current filtering system makes this one obsolete */
    function OptionalCriterionFilter(name, label) {
      this.name = name;
      this.label = label;
      var self = this;
      var argLen = arguments.length;
      var options = {};

      this.criterions = {};
      this.$elements = $("<div />").css("margin-bottom", "0.5em");

      var $select = $("<select />")
        .css("margin-left", "0.5em")
        .append($("<option />"));

      for (var i = 2; i < argLen; i++) {
        var args = arguments[i];
        var $option = $("<option />")
          .text(args.label)
          .data(args)
          .appendTo($select);
        options[args.value] = $option.get(0);
      }

      $select.on("change", function(e) {
        e.preventDefault();
        if (this.selectedIndex === 0) {
          /* this is empty option */
          return;
        }

        $(this)
          .children("option:selected")
          .each(function() {
            self.installOption(this);
          });
        this.selectedIndex = 0;
      });

      this.$elements.append($select);
    }

    function removeCriterion(e) {
      e.preventDefault();
      e.data.$container.remove();
      e.data.$option.show();
      delete e.data.instance.criterions[e.data.value];
      e.data.$container.trigger("redraw.DT");
    }

    function getValues(self) {
      var result = {
        selected_filters: [],
        values: {},
      };

      for (var filterName in self.criterions) {
        result.selected_filters.push(filterName);
        result.values[filterName] = self.criterions[filterName].save();
      }
      return result;
    }

    OptionalCriterionFilter.prototype = {
      getElements: function() {
        return this.$elements;
      },

      installOption: function(option) {
        var $option = $(option);
        var args = $option.data();
        var $container = $("<div />");

        $("<button />")
          .attr({ class: "close" })
          .append(
            $("<span />")
              .attr({ class: "glyphicon glyphicon-minus" })
              .text(args.label)
          )
          .on(
            "click",
            {
              instance: this,
              value: args.value,
              $option: $option,
              $container: $container,
            },
            removeCriterion
          )
          .appendTo($container);
        $("<input />")
          .attr({
            type: "hidden",
            name: this.name,
            value: args.value,
          })
          .appendTo($container);

        args.checked = true;
        var filterName = this.name + "." + args.value;
        var filterInstance = instantiateFilter(args.type, [
          filterName,
          "",
          args,
        ]);

        this.criterions[args.value] = filterInstance;
        $option.hide();
        this.$elements.append($container);
      },

      val: function() {
        return [JSON.stringify(getValues(this))];
      },

      save: function() {
        return [getValues(this)];
      },

      load: function(vals) {
        vals = vals[0];
        for (var filterName in vals.values) {
          if (this.options === null || !this.options[filterName]) {
            continue;
          }
          this.installOption(this.options[filterName]);
          this.criterions[filterName].load(vals.values[filterName]);
        }
      },

      hasValueSet: function() {
        /* jshint camelcase: false */
        return getValues(this).selected_filters.length > 0;
      },
    };

    return OptionalCriterionFilter;
  })();

  /*
   * Register a new feature with DataTables
   */
  if (
    typeof $.fn.dataTable === "function" &&
    typeof $.fn.dataTableExt.fnVersionCheck === "function" &&
    $.fn.dataTableExt.fnVersionCheck("1.7.0")
  ) {
    $.fn.dataTableExt.aoFeatures.push({
      fnInit: function(oDTSettings) {
        var asf = new AdvancedSearchFilters(oDTSettings);
        return asf.getContainer();
      },
      cFeature: "F",
      sFeature: "AdvancedSearchFilters",
    });
  } else {
    throw "Warning: AdvancedSearchFilters requires DataTables 1.7 or greater - www.datatables.net/download";
  }

  /*
   * setup useable href arguments according to current table filters criterions.
   * Used for CRM/Excel export
   */
  var dataTableSetExportArgs = function(e) {
    var tbl = $(e.target).dataTable();
    var settings = tbl.fnSettings();
    var params = tbl._fnAjaxParameters(settings);
    tbl._fnServerParams(params);
    $.data(e.target, "current-query-args", params);
    return false;
  };
  $.fn.dataTableSetExportArgs = dataTableSetExportArgs;
});
