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

<script setup lang="ts">
import {Tinyflow as TinyflowNative, TinyflowOptions} from '@tinyflow-ai/ui';
import '@tinyflow-ai/ui/dist/index.css';
import {
  getCurrentInstance,
  nextTick,
  onMounted,
  onUnmounted,
  ref,
  useAttrs,
  watch,
} from 'vue';

type TinyflowDataOption = Exclude<TinyflowOptions['data'], string | undefined>;
type StructuredCloneFn = <T>(value: T) => T;

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;
let lastAppliedDataSignature = '';
const optionalBooleanOptionKeys = new Set([
  'readonly',
  'hideBottomDock',
  'hideEdgePanel',
  'hideMiniMap',
  'hideNodeHandles',
  'hideNodeToolbar',
  'hideNodePicker',
  'hideNodeSetting',
  'hideEdgeMarkers',
  'edgeAnimated',
  'nodesDraggable',
  'nodesConnectable',
  'elementsSelectable',
  '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);
    }),
  );
}

// 安全深拷贝工具函数
function safeDeepClone<T>(obj: T): T {
  if (obj === null || typeof obj !== 'object') return obj;

  try {
    const clone = (globalThis as { structuredClone?: StructuredCloneFn })
      .structuredClone;
    if (clone) {
      return clone(obj);
    }
  } catch {
    // Fall through to JSON cloning for browsers without native structuredClone.
  }

  try {
    return JSON.parse(JSON.stringify(obj));
  } catch {
    try {
      console.warn(
        'Failed to clone object, returning original (may cause issues)',
        obj,
      );
      return obj;
    } catch {
      return obj;
    }
  }
}

function createDataSignature(data: unknown) {
  if (data == null || typeof data === 'string') {
    return String(data ?? '');
  }
  try {
    return JSON.stringify(data);
  } catch {
    return '';
  }
}

function cloneDataIfChanged(data: TinyflowOptions['data']) {
  if (data == null || typeof data === 'string') {
    return null;
  }
  const signature = createDataSignature(data);
  if (signature && signature === lastAppliedDataSignature) {
    return null;
  }
  lastAppliedDataSignature = signature;
  return safeDeepClone(data as TinyflowDataOption);
}

onMounted(() => {
  if (divRef.value) {
    // 净化 props.data，避免响应式对象或函数污染
    const cleanedProps = {
      ...normalizeOptions(attrs),
      ...normalizeProps(props),
    } as any;
    if ('data' in cleanedProps && cleanedProps.data != null) {
      cleanedProps.data = cloneDataIfChanged(cleanedProps.data);
    }

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

onUnmounted(() => {
  if (tinyflow) {
    tinyflow.destroy();
    tinyflow = null;
  }
  mountedDataReady = false;
  lastAppliedDataSignature = '';
});

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

watch(
  () => props.data,
  (data) => {
    if (
      tinyflow &&
      mountedDataReady &&
      data != null &&
      typeof data !== 'string'
    ) {
      const clonedData = cloneDataIfChanged(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 = cloneDataIfChanged(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>
