import { Controller } from '@hotwired/stimulus';
import { wpjEvent } from '@assets/components/util.event';

import { RESPONSE as EVENT_TYPE, LOADING } from '@assets/filters/common/events/eventTypes';

type ResponseEvent = CustomEvent<{ response: Record<string, any> }>;

/**
 * Example usage:
 * <div
 *    data-controller="filter-replace"
 *    data-filter-replace-target-value="outerHTML"                                              (what to replace on the element)
 *    data-filter-replace-key-value="productListHtml"                                           (the key in the response object)
 *
 *    data-filter-replace-emit-value="wpj-products-loaded"                                      OPTIONAL (emit an event after the response is received)
 *    data-filter-replace-emit-payload-value='{"type": "js-filters:product-list-replaced"}'     OPTIONAL (custom event payload)
 *    data-filter-replace-emit-target-value="#wpj-pager"                                        OPTIONAL (event target on which to dispatch; if not provided document is used)
 *
 *    data-filter-replace-remove-selector-value="[data-remove-me]"                              OPTIONAL (remove elements by selector after filter response)
 *
 *    data-filter-replace-use-skeleton-loader-value="true"                                      OPTIONAL (replaces content with skeletons of children on filter loading)
 * >
 *    <span data-replace-filter-target="replace"></span>                                        OPTIONAL (which element to replace if not defined, the element itself will be replaced)
 * </div>
 */
export default class extends Controller<HTMLElement> {
   static values = {
      replaceProp: String,
      responseKey: String,

      emit: String,
      emitPayload: Object,
      emitTarget: String,

      removeSelector: String,
      removeSelectorElement: String,

      useSkeletonLoader: Boolean,
   }

   private listener: ((ev: ResponseEvent) => void) | null = null;
   private loadingListener: ((ev: CustomEvent) => void) | null = null;

   static targets = ['replace'];

   declare hasReplaceTarget: boolean;
   declare replaceTarget: HTMLElement;

   declare replacePropValue: 'outerHTML' | 'innerText' | 'innerHTML';
   declare responseKeyValue: string;
   declare emitValue?: string;
   declare emitPayloadValue?: object;
   declare emitTargetValue?: string;
   declare removeSelectorValue?: string;
   declare useSkeletonLoaderValue?: boolean;

   connect() {
      this.listener = this.handleFilterResponse.bind(this);
      this.loadingListener = this.handleFilterLoading.bind(this);

      document.addEventListener(EVENT_TYPE, this.listener as EventListener);
      this.useSkeletonLoaderValue && document.addEventListener(LOADING, this.loadingListener as EventListener);
   }

   disconnect() {
      document.removeEventListener(EVENT_TYPE, this.listener as EventListener);
      this.useSkeletonLoaderValue && document.removeEventListener(LOADING, this.loadingListener as EventListener);

      this.listener = null;
      this.loadingListener = null;
   }

   private handleFilterResponse(ev: ResponseEvent) {
      const response = ev.detail.response;

      let targetResponseData: unknown = undefined;
      const responseKey = this.responseKeyValue;

      // Includes . = is a field in an inner object of the response
      if (responseKey.includes('.')) {
         for (const subKey of responseKey.split('.')) {
            if (!targetResponseData) {
               targetResponseData = response[subKey as keyof typeof response];
            } else {
               targetResponseData = targetResponseData[subKey as keyof typeof targetResponseData];
            }
         }
      } else {
         targetResponseData = response[responseKey as keyof typeof response];
      }

      if (typeof targetResponseData === 'undefined' || targetResponseData === null) {
         console.warn('[js-filters]', `Property not found in response: ${this.responseKeyValue}`);

         return;
      }

      let replaceTarget = this.element;
      if (this.hasReplaceTarget) {
         replaceTarget = this.replaceTarget;
      }

      if (targetResponseData instanceof Node) {
         replaceTarget.replaceWith(targetResponseData);
      } else {
         try {
            replaceTarget[this.replacePropValue || 'outerHTML'] = String(targetResponseData);
         } catch (e: unknown) {
            // Ignore `NoModificationAllowedError` -> no longer in the DOM?
            if (!(e instanceof Error) || e.name !== 'NoModificationAllowedError') {
               throw e;
            }
         }
      }

      this.emitValue && this.handleEmitEvent();
      this.removeSelectorValue && this.handleRemoveElements();
   }

   private handleEmitEvent() {
      let eventTarget: EventTarget | null = document;
      if (this.emitTargetValue) {
         eventTarget = document.querySelector(this.emitTargetValue);
      }

      if (!eventTarget) {
         eventTarget = document;
      }

      wpjEvent.trigger(
         eventTarget,
         this.emitValue!,
         this.emitPayloadValue || undefined,
      );
   }

   private handleRemoveElements() {
      for (const element of document.querySelectorAll(this.removeSelectorValue!)) {
         (element as HTMLElement).remove();
      }
   }

   private hidePagers() {
      for (const pager of document.querySelectorAll('[data-filter-pager]')) {
         (pager as HTMLDivElement).style.display = 'none';
      }
   }

   private handleFilterLoading(ev: CustomEvent<{ hidePagers: boolean }>) {
      const productLength = this.element.children.length;

      if (productLength < 1) {
         return;
      }

      const productElement = this.element.children[0];
      const skeletonElement = document.createElement('div');
      skeletonElement.className = `${productElement.className} skeleton`;
      skeletonElement.style.height = `${productElement.scrollHeight}px`;
      skeletonElement.style.width = `${productElement.scrollWidth}px`;
      const skeletonWrapper = [];

      for (let i = 1; i < productLength; i++) {
         skeletonWrapper.push(skeletonElement.cloneNode());
      }

      ev.detail.hidePagers && this.hidePagers();
      this.element.innerHTML = '';
      this.element.append(...skeletonWrapper);
   }
}
