import type { Newable } from '@difizen/mana-common';
import { Emitter } from '@difizen/mana-common';
import { Deferred } from '@difizen/mana-common';
import { Disposable, Priority } from '@difizen/mana-common';
import { prop } from '@difizen/mana-observable';
import { contrib, inject, singleton } from '@difizen/mana-syringe';
import type { Contribution } from '@difizen/mana-syringe';

import { DebugService } from '../common/debug';

import { DefaultSlotView } from './default-slot-view';
import { ViewManager } from './view-manager';
import type { View } from './view-protocol';
import type { SlotPreference, ViewOpenOption } from './view-protocol';
import { SlotPreferenceContribution, SlotView } from './view-protocol';

interface ViewAddArgs {
  view?: View;
  slot: string;
  option?: ViewOpenOption;
}

interface SlotSetArgs {
  view?: View;
  slot: string;
}

@singleton()
export class SlotViewManager {
  protected preferences: Map<string, SlotPreference> = new Map();
  protected componentPreferences: Map<string, SlotPreference> = new Map();
  protected slotRenderingDeferred: Map<string, Deferred<void>> = new Map();

  protected onViewAddedEmitter: Emitter<ViewAddArgs> = new Emitter<ViewAddArgs>();
  protected onSlotChangedEmitter: Emitter<SlotSetArgs> = new Emitter<SlotSetArgs>();

  get onViewAdded() {
    return this.onViewAddedEmitter.event;
  }

  get onSlotChanged() {
    return this.onSlotChangedEmitter.event;
  }

  /**
   * slot -> slotview
   */
  @prop()
  slotViewMap: Map<string, View> = new Map<string, View>();

  /**
   * view -> child slotview
   */
  @prop()
  slotChildrenMap: Map<View, string[]> = new Map<View, string[]>();

  protected readonly preferenceProvider: Contribution.Provider<SlotPreferenceContribution>;
  protected readonly debugService: DebugService;
  protected readonly viewManager: ViewManager;

  constructor(
    @contrib(SlotPreferenceContribution)
    preferenceProvider: Contribution.Provider<SlotPreferenceContribution>,
    @inject(DebugService) debugService: DebugService,
    @inject(ViewManager) viewManager: ViewManager,
  ) {
    this.preferenceProvider = preferenceProvider;
    this.debugService = debugService;
    this.viewManager = viewManager;
    this.setPreferencesFromContribution();
  }

  protected setPreferencesFromContribution(): void {
    for (const preference of this.preferenceProvider.getContributions()) {
      preference.forEach((item) => {
        const { slot } = item;
        this.setSlotPreference(slot, item);
      });
    }
  }

  protected getPreferences(): Map<string, SlotPreference> {
    return this.preferences;
  }

  setSlotPreference(slot: string, preference: SlotPreference): void {
    const last = this.preferences.get(slot);
    const currentPriority = preference.priority ?? Priority.IDLE;
    if (!last || !last.priority || currentPriority > last.priority) {
      this.preferences.set(slot, preference);
    }
  }

  setComponentSlotPreference(slot: string, preference: SlotPreference): void {
    const last = this.componentPreferences.get(slot);
    const currentPriority = preference.priority ?? Priority.IDLE;
    if (!last || !last.priority || currentPriority > last.priority) {
      this.componentPreferences.set(slot, preference);
    }
  }

  protected waitSlotRendering(slot: string): Promise<void> {
    let deferred = this.slotRenderingDeferred.get(slot);
    if (!deferred) {
      deferred = new Deferred<void>();
      this.slotRenderingDeferred.set(slot, deferred);
    }
    return deferred.promise;
  }

  slotRendering(slot: string): void {
    let deferred = this.slotRenderingDeferred.get(slot);
    if (!deferred) {
      deferred = new Deferred<void>();
      this.slotRenderingDeferred.set(slot, deferred);
    }
    deferred.resolve();
  }

  async getOrCreateSlotView(
    slot: string,
    defaultSlotView: Newable<View> = DefaultSlotView,
  ): Promise<SlotView | View> {
    const existSlotView = this.slotViewMap.get(slot);
    if (existSlotView) {
      return existSlotView;
    }
    let preference = this.preferences.get(slot);
    if (!preference) {
      await this.waitSlotRendering(slot);
      preference = this.componentPreferences.get(slot);
    }
    const defaultSlotViewFactory = this.viewManager.getFactory(DefaultSlotView)!;
    const toFactory = preference?.view ?? defaultSlotView ?? DefaultSlotView;
    const slotViewFactory =
      this.viewManager.getFactory(toFactory) ?? defaultSlotViewFactory;
    const slotOption = { ...preference?.options, area: slot };
    const slotView = await this.viewManager.getOrCreateView(
      slotViewFactory.id,
      slotOption,
    );
    slotView.onDisposed(this.setSlotView(slot, slotView).dispose);
    this.debugService('slot view set', slot, slotView);
    return slotView as SlotView;
  }

  getSlotView(slot: string): View | undefined {
    return this.slotViewMap.get(slot);
  }

  setSlotView(slot: string, view: View): Disposable {
    this.slotViewMap.set(slot, view);
    this.onSlotChangedEmitter.fire({ view, slot });
    return Disposable.create(() => {
      if (this.slotViewMap.get(slot) === view) {
        this.slotViewMap.delete(slot);
        this.onSlotChangedEmitter.fire({ view: undefined, slot });
      }
    });
  }

  async addView(view: View, slot: string, option?: ViewOpenOption): Promise<void> {
    const slotView = await this.getOrCreateSlotView(slot);
    if (SlotView.is(slotView)) {
      await slotView.addView(view, option);
      this.onViewAddedEmitter.fire({ view, slot, option });
    }
  }

  async removeView(view: View, slot: string): Promise<void> {
    const slotView = await this.getOrCreateSlotView(slot);
    if (SlotView.is(slotView)) {
      await slotView.removeView(view);
    }
  }

  hasSlot(slot: string): boolean {
    return this.slotViewMap.has(slot);
  }

  getSlotChildren(view: View): string[] | undefined {
    return this.slotChildrenMap.get(view);
  }
  addSlotToView(slot: string, slotView: SlotView): void {
    this.slotChildrenMap.set(slotView, [
      ...(this.slotChildrenMap.get(slotView) ?? []),
      slot,
    ]);
  }
  removeSlotFromView(slot: string, slotView: SlotView): void {
    const children = this.slotChildrenMap.get(slotView);
    if (children) {
      this.slotChildrenMap.set(
        slotView,
        children.filter((child) => child !== slot),
      );
    }
  }
}
