/*
 * Copyright (C) 2022 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */

import {ComponentRef, Directive, ElementRef, OnDestroy, OnInit, ViewContainerRef} from '@angular/core';
import {IonIcon} from '@ionic/angular';
import {IonIconDirective} from './ion-icon.directive';

export type IconData = Omit<
  Partial<IonIconDirective>,
  'ngOnChanges' | 'ngOnInit' | 'viewContainerRef' | 'ngOnDestroy' | 'element' | 'ionIcon' | 'disableProperty'
>;

/**
 * A utility class to replace ion-icons in other ionic components.
 */
@Directive()
export abstract class IconReplacer implements OnInit, OnDestroy {
  private mutationObserver: MutationObserver;

  protected slotName = 'sc-icon';

  protected maxAttempts = 10;

  protected retryAfterMs = 10;

  /**
   * The host element
   *
   * This will be either element.nativeElement.shadowRoot or element.nativeElement
   * depending on the iconDomLocation
   */
  protected get host() {
    return this.iconDomLocation === 'shadow'
      ? this.element.nativeElement.shadowRoot
      : this.element.nativeElement;
  }

  /**
   * @param element The host element
   * @param viewContainerRef The view container ref
   * @param iconDomLocation If the icon is placed inside the shadow dom or not
   * @protected
   */
  protected constructor(
    private readonly element: ElementRef,
    private readonly viewContainerRef: ViewContainerRef,
    private readonly iconDomLocation: 'shadow' | 'light',
  ) {}

  /**
   * Replace the icons here
   */
  abstract replace(): void;

  /**
   * If any additional work needs to be done, this
   * is called during ngOnInit
   */
  init() {
    // noop
  }

  /**
   * If you need to do cleanup, this method is called during ngOnDestroy
   */
  destroy() {
    // noop
  }

  ngOnInit() {
    this.init();

    if (this.host) {
      this.attachObserver();
    } else {
      let tries = 0;
      console.warn('IconReplacer: host not found, trying again');
      const interval = setInterval(() => {
        if (tries > this.maxAttempts) {
          clearInterval(interval);
          throw new Error('IconReplacer: host not found');
        }
        if (this.host) {
          clearInterval(interval);
          this.replace();
        }
        tries++;
      }, this.retryAfterMs);
    }
  }

  private attachObserver() {
    this.mutationObserver = new MutationObserver(() => this.replace());
    this.mutationObserver.observe(this.host, {
      childList: true,
    });
  }

  replaceIcon(parent: HTMLElement | null, iconData: IconData, slotName = '') {
    if (!parent) return;

    const icon = parent.querySelector('ion-icon');
    if (!icon) return;

    const scIcon = this.createIcon(iconData);
    // @ts-expect-error can be spread
    scIcon.location.nativeElement.classList.add(...icon.classList);

    if (this.iconDomLocation === 'shadow') {
      // shadow dom needs to utilize slotting, to put it outside
      // the shadow dom, otherwise it won't receive any css data
      const slot = document.createElement('slot');
      slot.name = this.slotName + slotName;
      icon.replaceWith(slot);

      scIcon.location.nativeElement.slot = this.slotName + slotName;
      this.element.nativeElement.append(scIcon.location.nativeElement);
    } else {
      icon.replaceWith(scIcon.location.nativeElement);
    }
  }

  private createIcon(iconData: IconData): ComponentRef<IonIcon> {
    const ionIcon = this.viewContainerRef.createComponent(IonIcon, {});
    const iconDirective = new IonIconDirective(ionIcon.location, this.viewContainerRef, ionIcon.instance);
    for (const key in iconData) {
      // @ts-expect-error type mismatch
      iconDirective[key] = iconData[key];
    }
    iconDirective.ngOnInit();

    return ionIcon;
  }

  ngOnDestroy() {
    this.mutationObserver?.disconnect();
    this.destroy();
  }
}
