import { Controller } from "@hotwired/stimulus"
require("select2/dist/css/select2.min.css");
import select2 from "select2";

const select2SelectedPreviewSelector = ".select2-selection--single"
const select2SearchInputFieldSelector = ".select2-search__field"

export default class extends Controller {
  static targets = [ "select" ]
  static values = {
    acceptsNew: Boolean,
    enableSearch: Boolean,
    searchUrl: String,
  }
  
  // will be reissued as native dom events name prepended with '$' e.g. '$change', '$select2:closing', etc
  static jQueryEventsToReissue = [ "change", "select2:closing", "select2:close", "select2:opening", "select2:open", "select2:selecting", "select2:select", "select2:unselecting", "select2:unselect", "select2:clearing", "select2:clear" ]
  

  initialize() {
    this.dispatchNativeEvent = this.dispatchNativeEvent.bind(this)
    if (!this.isSelect2LoadedOnWindowJquery) {
      select2()
    }
  }

  get isSelect2LoadedOnWindowJquery() {
    return window?.$?.fn?.select2 !== undefined
  }

  connect() {
    if (this.isSelect2LoadedOnWindowJquery) {
      this.initPluginInstance()
    }
  }

  disconnect() {
    this.teardownPluginInstance()
  }

  cleanupBeforeInit() {
    $(this.element).find('.select2-container--default').remove()
  }

  initPluginInstance() {
    let options = {
      dropdownParent: $(this.element)
    };

    if (!this.enableSearchValue) {
      options.minimumResultsForSearch = -1;
    }

    options.tags = this.acceptsNewValue

    if (this.searchUrlValue) {
      options.ajax = {
        url: this.searchUrlValue,
        dataType: 'json',
        // We enable pagination by default here
        data: function(params) {
          var query = {
            search: params.term,
            page: params.page || 1
          }
          return query
        }
        // Any additional params go here...
      }
    }

    options.templateResult = this.formatState;
    options.templateSelection = this.formatState;
    options.width = 'style';

    this.cleanupBeforeInit() // in case improperly torn down
    this.pluginMainEl = this.selectTarget // required because this.selectTarget is unavailable on disconnect()
    $(this.pluginMainEl).select2(options);

    this.initReissuePluginEventsAsNativeEvents()
  }

  teardownPluginInstance() {
    if (this.pluginMainEl === undefined) { return }

    // ensure there are no orphaned event handlers
    this.teardownPluginEventsAsNativeEvents()
    
    // revert to original markup, remove any event listeners
    $(this.pluginMainEl).select2('destroy');
  }
  
  open() {
    $(this.pluginMainEl).select2('open')
  }
  
  focusOnTextField(event) {
    this.element.querySelector(select2SearchInputFieldSelector)?.focus()
  }
  
  injectKeystrokeIntoTextField(event) {
    if (!event?.srcElement.matches(select2SelectedPreviewSelector)) { return }
    
    if (["Shift", "Alt", "Control", "Meta", "Tab", "Backspace", "Escape"].includes(event.key)) { return }
    
    this.open()
    
    const searchInputField = this.element.querySelector(select2SearchInputFieldSelector)
    
    if (!searchInputField) { return }
    
    if (event.type !== "keydown") {
      // since keydown precedes keyup, and since keyup is what sends the key to an input field, this next line isn't necessary if keydown is the event captured. We'll just focus on the input field, and the keyup will caught by the input field naturally.
      searchInputField.value = searchInputField.value + event.key
    }
    searchInputField.focus()
  }

  initReissuePluginEventsAsNativeEvents() {
    this.constructor.jQueryEventsToReissue.forEach((eventName) => {
      $(this.pluginMainEl).on(eventName, this.dispatchNativeEvent)
    })
  }
  
  teardownPluginEventsAsNativeEvents() {
    this.constructor.jQueryEventsToReissue.forEach((eventName) => {
      $(this.pluginMainEl).off(eventName)
    })
  }
  
  dispatchNativeEvent(event) {
    const nativeEventName = '$' + event.type // e.g. '$change.select2'
    this.element.dispatchEvent(new CustomEvent(nativeEventName, { detail: { event: event }, bubbles: true, cancelable: false }))
  }

  // https://stackoverflow.com/questions/29290389/select2-add-image-icon-to-option-dynamically
  formatState(opt) {
    var imageUrl = $(opt.element).attr('data-image');
    var imageHtml = "";
    if (imageUrl) {
      imageHtml = '<img src="' + imageUrl + '" /> ';
    }
    return $('<span>' + imageHtml + sanitizeHTML(opt.text) + '</span>');
  }
}

// https://portswigger.net/web-security/cross-site-scripting/preventing
function sanitizeHTML(str) {
  return str.replace(/[^\w. ]/gi, function (c) {
    return '&#' + c.charCodeAt(0) + ';';
  });
};