/*
 * Tencent is pleased to support the open source community by making TMagicEditor available.
 *
 * Copyright (C) 2025 Tencent.  All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { EventEmitter } from 'events';

import { snapdom, SnapdomOptions } from '@zumer/snapdom';

import type { Id } from '@tmagic/core';
import { getElById, getHost, guid, injectStyle, isSameDomain } from '@tmagic/core';

import { DEFAULT_ZOOM, RenderType } from './const';
import style from './style.css?raw';
import type { Point, RemoveData, RenderEvents, Runtime, RuntimeWindow, StageRenderConfig, UpdateData } from './types';
import { addSelectedClassName, removeSelectedClassName } from './util';

export default class StageRender extends EventEmitter {
  /** 组件的js、css执行的环境，直接渲染为当前window，iframe渲染则为iframe.contentWindow */
  public contentWindow: RuntimeWindow | null = null;
  public runtime: Runtime | null = null;
  public iframe?: HTMLIFrameElement;
  public nativeContainer?: HTMLDivElement;

  private runtimeUrl?: string;
  private zoom = DEFAULT_ZOOM;
  private renderType: RenderType;
  private customizedRender?: () => Promise<HTMLElement | null | void>;

  constructor({ runtimeUrl, zoom, customizedRender, renderType = RenderType.IFRAME }: StageRenderConfig) {
    super();

    this.renderType = renderType;
    this.runtimeUrl = runtimeUrl || '';
    this.customizedRender = customizedRender;
    this.setZoom(zoom);

    if (this.renderType === RenderType.IFRAME) {
      this.createIframe();
    } else if (this.renderType === RenderType.NATIVE) {
      this.createNativeContainer();
    }
  }

  public getMagicApi = () => ({
    id: guid(),
    onPageElUpdate: (el: HTMLElement) => {
      this.emit('page-el-update', el);
    },
    onRuntimeReady: (runtime: Runtime) => {
      if (this.runtime) {
        return;
      }

      this.runtime = runtime;
      // @ts-ignore
      globalThis.runtime = runtime;
      this.emit('runtime-ready', runtime);
    },
  });

  public async add(data: UpdateData): Promise<void> {
    const runtime = await this.getRuntime();
    return runtime?.add?.(data);
  }

  public async remove(data: RemoveData): Promise<void> {
    const runtime = await this.getRuntime();
    return runtime?.remove?.(data);
  }

  public async update(data: UpdateData): Promise<void> {
    const runtime = await this.getRuntime();
    // 更新画布中的组件
    runtime?.update?.(data);
  }

  public async select(ids: Id[]): Promise<void> {
    const runtime = await this.getRuntime();

    for (const id of ids) {
      await runtime?.select?.(id);

      this.flagSelectedEl(this.getTargetElement(id));
    }
  }

  public setZoom(zoom: number = DEFAULT_ZOOM): void {
    this.zoom = zoom;
  }

  /**
   * 挂载Dom节点
   * @param el 将页面挂载到该Dom节点上
   */
  public async mount(el: HTMLDivElement) {
    if (this.iframe) {
      if (!isSameDomain(this.runtimeUrl) && this.runtimeUrl) {
        // 不同域，使用srcdoc发起异步请求，需要目标地址支持跨域
        let html = await fetch(this.runtimeUrl).then((res) => res.text());
        // 使用base, 解决相对路径或绝对路径的问题
        const base = `${location.protocol}//${getHost(this.runtimeUrl)}`;
        html = html.replace('<head>', `<head>\n<base href="${base}">`);
        this.iframe.srcdoc = html;
      }

      el.appendChild<HTMLIFrameElement>(this.iframe);

      this.postTmagicRuntimeReady();
    } else if (this.nativeContainer) {
      el.appendChild(this.nativeContainer);
    }
  }

  public getRuntime = (): Promise<Runtime> => {
    if (this.runtime) return Promise.resolve(this.runtime);
    return new Promise((resolve) => {
      const listener = (runtime: Runtime) => {
        this.off('runtime-ready', listener);
        resolve(runtime);
      };
      this.on('runtime-ready', listener);
    });
  };

  public getDocument(): Document | undefined {
    return this.contentWindow?.document;
  }

  /**
   * 通过坐标获得坐标下所有HTML元素数组
   * @param point 坐标
   * @returns 坐标下方所有HTML元素数组，会包含父元素直至html，元素层叠时返回顺序是从上到下
   */
  public getElementsFromPoint(point: Point): HTMLElement[] {
    let x = point.clientX;
    let y = point.clientY;

    if (this.iframe) {
      const rect = this.iframe.getClientRects()[0];
      if (rect) {
        x = x - rect.left;
        y = y - rect.top;
      }
    }

    return this.getDocument()?.elementsFromPoint(x / this.zoom, y / this.zoom) as HTMLElement[];
  }

  public getTargetElement(id: Id): HTMLElement | null {
    return getElById()(this.getDocument(), id);
  }

  /**
   * 将指定id的dom元素生成为图片
   * @param id 目标元素的id
   * @param options 图片生成选项
   * @returns data URL 格式的图片数据
   */
  public async getElementImage(
    id: Id,
    type: 'download' | 'raw' | 'svg' | 'canvas' | 'png' | 'jpeg' | 'webp' | 'blob' = 'png',
    options: SnapdomOptions = {},
  ) {
    const el = this.getTargetElement(id);
    if (!el) {
      throw new Error(`Element with id "${id}" not found`);
    }

    el.scrollIntoView();

    const toFunc = `to${type.charAt(0).toUpperCase() + type.slice(1)}`;

    const result = await snapdom(el, options);

    if (toFunc in result) {
      return result[toFunc]();
    }

    throw new Error(`Invalid type: ${type}`);
  }

  public postTmagicRuntimeReady() {
    this.contentWindow = this.iframe?.contentWindow as RuntimeWindow;

    this.contentWindow.magic = this.getMagicApi();

    this.contentWindow.postMessage(
      {
        tmagicRuntimeReady: true,
      },
      '*',
    );
  }

  public reloadIframe(url: string) {
    if (this.renderType !== RenderType.IFRAME) return;

    const el = this.iframe?.parentElement;
    this.destroyIframe();
    this.runtimeUrl = url;
    this.createIframe();
    this.mount(el as HTMLDivElement);
    this.runtime = null;
  }

  public destroyIframe() {
    this.iframe?.removeEventListener('load', this.iframeLoadHandler);
    this.contentWindow = null;
    this.iframe?.remove();
    this.iframe = undefined;
  }

  /**
   * 销毁实例
   */
  public destroy(): void {
    this.destroyIframe();
    // @ts-ignore
    globalThis.runtime = undefined;
    this.removeAllListeners();
  }

  public on<Name extends keyof RenderEvents, Param extends RenderEvents[Name]>(
    eventName: Name,
    listener: (...args: Param) => void | Promise<void>,
  ) {
    return super.on(eventName, listener as any);
  }

  public emit<Name extends keyof RenderEvents, Param extends RenderEvents[Name]>(eventName: Name, ...args: Param) {
    return super.emit(eventName, ...args);
  }

  private createIframe(): HTMLIFrameElement {
    this.iframe = globalThis.document.createElement('iframe');
    // 同源，直接加载
    this.iframe.src = this.runtimeUrl && isSameDomain(this.runtimeUrl) ? this.runtimeUrl : '';
    this.iframe.style.cssText = `
      border: 0;
      width: 100%;
      height: 100%;
    `;

    this.iframe.addEventListener('load', this.iframeLoadHandler);

    return this.iframe;
  }

  private async createNativeContainer() {
    this.contentWindow = globalThis as unknown as RuntimeWindow;
    this.nativeContainer = globalThis.document.createElement('div');

    this.contentWindow.magic = this.getMagicApi();

    if (this.customizedRender) {
      const el = await this.customizedRender();
      if (el) {
        this.nativeContainer.appendChild(el);
      }
    }
  }

  /**
   * 在runtime中对被选中的元素进行标记，部分组件有对选中态进行特殊显示的需求
   * @param el 被选中的元素
   */
  private flagSelectedEl(el: HTMLElement | null): void {
    const doc = this.getDocument();
    if (doc) {
      removeSelectedClassName(doc);
      el && addSelectedClassName(el, doc);
    }
  }

  private iframeLoadHandler = () => {
    const handler = async () => {
      if (!this.contentWindow?.magic) {
        this.postTmagicRuntimeReady();
      }

      if (!this.contentWindow) return;

      if (this.customizedRender) {
        const el = await this.customizedRender();
        if (el) {
          this.contentWindow.document?.body?.appendChild(el);
        }
      }

      this.emit('onload');

      injectStyle(this.contentWindow.document, style);
    };

    handler();
  };
}
