import type {useSvelteFlow} from '@xyflow/svelte';
import {componentName} from './consts';
import {store} from './store/stores.svelte';
import type {TinyflowData, TinyflowOptions, TinyflowTheme} from './types';
import {installTinyflowBrowserCompat} from './utils/compat';
import {createTinyflowNodeNormalizer} from './utils/nodeInteraction';

installTinyflowBrowserCompat();

type FlowInstance = ReturnType<typeof useSvelteFlow>;

export class Tinyflow {
  private options!: TinyflowOptions;
  private rootEl!: Element;
  private svelteFlowInstance!: FlowInstance;
  private tinyflowEl!: HTMLElement & {
    options: TinyflowOptions;
    onInit: (svelteFlowInstance: FlowInstance) => void;
  };

  constructor(options: TinyflowOptions) {
    if (
      typeof options.element !== 'string' &&
      !(options.element instanceof Element)
    ) {
      throw new Error('element must be a string or Element');
    }
    this._setOptions(options);
    this._init();
  }

  private _init() {
    if (typeof this.options.element === 'string') {
      this.rootEl = document.querySelector(this.options.element)!;
      if (!this.rootEl) {
        throw new Error(
          `element not found by document.querySelector('${this.options.element}')`,
        );
      }
    } else if (this.options.element instanceof Element) {
      this.rootEl = this.options.element;
    } else {
      throw new Error('element must be a string or Element');
    }

    this.tinyflowEl = this._createTinyflowElement();
    this.rootEl.appendChild(this.tinyflowEl);
  }

  private _setOptions(options: TinyflowOptions) {
    this.options = {
      theme: options.theme || 'light',
      ...options,
    };
  }

  private _getFlowInstance() {
    if (!this.svelteFlowInstance) {
      console.warn('Tinyflow instance is not initialized');
      return null;
    }
    return this.svelteFlowInstance;
  }

  private _applyThemeClass(targetEl: Element, theme?: TinyflowTheme) {
    targetEl.classList.remove('tf-theme-light', 'tf-theme-dark');
    targetEl.classList.add(
      theme === 'dark' ? 'tf-theme-dark' : 'tf-theme-light',
    );
  }

  private _createTinyflowElement() {
    const tinyflowEl = document.createElement(componentName) as HTMLElement & {
      options: TinyflowOptions;
      onInit: (svelteFlowInstance: FlowInstance) => void;
    };
    tinyflowEl.style.display = 'block';
    tinyflowEl.style.width = '100%';
    tinyflowEl.style.height = '100%';
    this._applyThemeClass(tinyflowEl, this.options.theme);

    tinyflowEl.options = this.options;
    tinyflowEl.onInit = (svelteFlowInstance: FlowInstance) => {
      this.svelteFlowInstance = svelteFlowInstance;
    };
    return tinyflowEl;
  }

  getOptions() {
    return this.options;
  }

  getData() {
    const flow = this._getFlowInstance();
    if (!flow) {
      return null;
    }
    return flow.toObject();
  }

  updateData(data: TinyflowData, options?: { preserveViewport?: boolean }) {
    const flow = this._getFlowInstance();
    if (!flow) {
      return false;
    }

    const currentViewport = flow.getViewport();
    const currentNodes = flow.getNodes();
    const normalizeNode = createTinyflowNodeNormalizer(this.options);
    const currentNodePositions = new Map(
      currentNodes.map((node) => [node.id, node.position]),
    );
    const nextNodes =
      options?.preserveViewport === true
        ? (data.nodes || currentNodes).map((node) => {
            const currentPosition = currentNodePositions.get(node.id);
            const nextNode = currentPosition
              ? { ...node, position: { ...currentPosition } }
              : node;
            return normalizeNode(nextNode);
          })
        : (data.nodes || currentNodes).map((node) => normalizeNode(node));
    store.setNodes(nextNodes);
    store.setEdges(data.edges || flow.getEdges());

    if (data.viewport && options?.preserveViewport !== true) {
      flow.setViewport(data.viewport, { duration: 0 });
    } else {
      flow.setViewport(currentViewport, { duration: 0 });
    }
    return true;
  }

  async focusNode(
    nodeId: string,
    options?: { duration?: number; zoom?: number },
  ) {
    const flow = this._getFlowInstance();
    if (!flow) {
      return false;
    }

    const targetNode = flow.getNode(nodeId);
    if (!targetNode) {
      return false;
    }

    // Keep only the target node selected so the canvas has a clear visual focus.
    flow.getNodes().forEach((node) => {
      flow.updateNode(node.id, { selected: node.id === nodeId });
    });

    const internalNode = flow.getInternalNode(nodeId) as any;
    const absolutePosition = internalNode?.internals?.positionAbsolute ||
      (targetNode as any)?.positionAbsolute ||
      targetNode.position || { x: 0, y: 0 };
    const width =
      internalNode?.measured?.width ||
      (targetNode as any)?.measured?.width ||
      (targetNode as any)?.width ||
      260;
    const height =
      internalNode?.measured?.height ||
      (targetNode as any)?.measured?.height ||
      (targetNode as any)?.height ||
      120;

    const centerX = absolutePosition.x + width / 2;
    const centerY = absolutePosition.y + height / 2;
    const nextZoom = options?.zoom ?? Math.max(flow.getZoom(), 1);

    await flow.setCenter(centerX, centerY, {
      zoom: nextZoom,
      duration: options?.duration ?? 280,
    });

    return true;
  }

  async fitView(options?: { duration?: number; padding?: number }) {
    const flow = this._getFlowInstance();
    if (!flow) {
      return false;
    }
    await flow.fitView({
      duration: options?.duration ?? 220,
      padding: options?.padding ?? 0.2,
    });
    return true;
  }

  setTheme(theme: TinyflowTheme) {
    this.options.theme = theme;
    if (this.tinyflowEl) {
      this._applyThemeClass(this.tinyflowEl, theme);
    }
  }

  setData(data: TinyflowData) {
    this.options.data = data;

    this.tinyflowEl = this._createTinyflowElement();

    this.destroy();
    this.rootEl.appendChild(this.tinyflowEl);
  }

  destroy() {
    while (this.rootEl.firstChild) {
      this.rootEl.removeChild(this.rootEl.firstChild);
    }
  }
}
