<template>
  <div ref="divRef" :class="['tinyflow', className]" :style="style" />
</template>

<script setup lang="ts">
import type { TinyflowOptions } from '@tinyflow-ai/ui';

import { Tinyflow as TinyflowNative } from '@tinyflow-ai/ui';
import '@tinyflow-ai/ui/dist/index.css';
import {
  getCurrentInstance,
  nextTick,
  onMounted,
  onUnmounted,
  ref,
  useAttrs,
  watch,
} from 'vue';

import { createTinyflowDataSync } from './dataSync';

const props = defineProps<
  {
    className?: string;
    style?: Record<string, string>;
  } & Omit<TinyflowOptions, 'element'>
>();

const divRef = ref<HTMLDivElement | null>(null);
const attrs = useAttrs();
const instance = getCurrentInstance();
let tinyflow: TinyflowNative | null = null;
let mountedDataReady = false;
const dataSync = createTinyflowDataSync();
const optionalBooleanOptionKeys = new Set([
  'readonly',
  'hideBottomDock',
  'hideEdgePanel',
  'hideMiniMap',
  'hideNodeHandles',
  'hideNodeToolbar',
  'hideNodePicker',
  'hideNodeSetting',
  'hideEdgeMarkers',
  'edgeAnimated',
  'nodesDraggable',
  'nodesConnectable',
  'elementsSelectable',
  'onlyRenderVisibleElements',
  'dropEnabled',
]);

function normalizeOptionKey(key: string) {
  return key.replace(/-([a-z])/g, (_match: string, letter: string) =>
    letter.toUpperCase(),
  );
}

function normalizeOptions(source: Record<string, unknown>) {
  return Object.fromEntries(
    Object.entries(source).map(([key, value]) => [
      normalizeOptionKey(key),
      value,
    ]),
  );
}

function getProvidedPropKeys() {
  return new Set(
    Object.keys(instance?.vnode.props || {}).map((key) => normalizeOptionKey(key)),
  );
}

function normalizeProps(source: Record<string, unknown>) {
  const providedKeys = getProvidedPropKeys();
  return Object.fromEntries(
    Object.entries(source).filter(([key, value]) => {
      if (!optionalBooleanOptionKeys.has(key)) {
        return true;
      }
      return value !== false || providedKeys.has(key);
    }),
  );
}

onMounted(() => {
  if (divRef.value) {
    // 净化 props.data，避免响应式对象或函数污染
    const cleanedProps = {
      ...normalizeOptions(attrs),
      ...normalizeProps(props),
    } as any;
    if ('data' in cleanedProps && cleanedProps.data != null) {
      cleanedProps.data = dataSync.cloneExternalDataIfChanged(cleanedProps.data);
    }
    (['onDataChange', 'onDataCommit'] as const).forEach((callbackKey) => {
      const callback = cleanedProps[callbackKey];
      if (typeof callback !== 'function') {
        return;
      }
      cleanedProps[callbackKey] = (
        data: TinyflowOptions['data'],
        ...args: unknown[]
      ) => {
        dataSync.markNativeData(data);
        callback(data, ...args);
      };
    });

    tinyflow = new TinyflowNative({
      ...cleanedProps,
      element: divRef.value,
    });
    mountedDataReady = true;
  }
});

onUnmounted(() => {
  if (tinyflow) {
    tinyflow.destroy();
    tinyflow = null;
  }
  mountedDataReady = false;
  dataSync.reset();
});

watch(
  () => props.theme,
  (theme) => {
    if (tinyflow) {
      tinyflow.setTheme(theme || 'light');
    }
  },
);

watch(
  () => props.data,
  (data) => {
    if (
      tinyflow &&
      mountedDataReady &&
      data != null &&
      typeof data !== 'string'
    ) {
      const clonedData = dataSync.cloneExternalDataIfChanged(data);
      if (clonedData) {
        tinyflow.updateData(clonedData, { preserveViewport: true });
      }
    }
  },
  { deep: true },
);

const getData = () => {
  if (tinyflow) {
    return tinyflow.getData();
  }
  console.warn('Tinyflow instance is not initialized');
  return null;
};

const getInstance = () => {
  if (tinyflow) {
    return tinyflow;
  }
  console.warn('Tinyflow instance is not initialized');
  return null;
};

const focusNode = async (
  nodeId: string,
  options?: { duration?: number; zoom?: number },
) => {
  if (tinyflow) {
    return tinyflow.focusNode(nodeId, options);
  }
  console.warn('Tinyflow instance is not initialized');
  return false;
};

const fitView = async (options?: { duration?: number; padding?: number }) => {
  if (tinyflow) {
    await nextTick();
    return tinyflow.fitView(options);
  }
  console.warn('Tinyflow instance is not initialized');
  return false;
};

const updateData = (
  data: TinyflowOptions['data'],
  options?: { preserveViewport?: boolean },
) => {
  if (tinyflow && data != null && typeof data !== 'string') {
    const clonedData = dataSync.cloneExternalDataIfChanged(data);
    if (!clonedData) {
      return true;
    }
    return tinyflow.updateData(clonedData, options);
  }
  console.warn('Tinyflow instance is not initialized');
  return false;
};

defineExpose({
  getData,
  getInstance,
  focusNode,
  fitView,
  updateData,
});
</script>
