/*
 * Copyright 2025 coze-dev Authors
 *
 * 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.
 */

/* eslint-disable @coze-arch/max-line-per-function */
import { useCallback, useEffect, useRef, useState } from 'react';

import { cloneDeep } from 'lodash-es';
import { type Canvas, type CanvasEvents, type FabricObject } from 'fabric';
import { useLatest } from 'ahooks';
import {
  ViewVariableType,
  type InputVariable,
} from '@coze-workflow/base/types';
import { I18n } from '@coze-arch/i18n';
import { getUploadCDNAsset } from '@coze-workflow/base-adapter';

import { createElement, defaultProps } from '../utils';
import {
  Mode,
  UNKNOWN_VARIABLE_NAME,
  type FabricObjectWithCustomProps,
  type FabricSchema,
  type VariableRef,
} from '../typings';

const ImagePlaceholder = `${getUploadCDNAsset(
  '',
)}/workflow/fabric-canvas/img-placeholder.png`;

// Properties that require additional saving
export const saveProps = [
  'width',
  'height',
  'editable',
  'text',
  'backgroundColor',
  'padding',
  // custom parameters
  //  The true height of the textBox
  'customFixedHeight',
  // Element ID
  'customId',
  // element type
  'customType',
  // Adaptive mode of image
  'customFixedType',
  // The title of the element generated by the variable
  // reference relationship
  'customVariableRefs',
];

export const useCanvasChange = ({
  variables,
  canvas,
  onChange,
  schema,
  listenerEvents = [
    'object:modified',
    'object:added',
    'object:removed',
    'object:moving',
    'object:modified-zIndex',
  ],
}: {
  variables?: InputVariable[];
  canvas?: Canvas;
  onChange?: (schema: FabricSchema) => void;
  schema?: FabricSchema;
  listenerEvents?: (
    | 'object:modified'
    | 'object:added'
    | 'object:removed'
    | 'object:moving'
    | 'object:modified-zIndex'
  )[];
}) => {
  const eventDisposers = useRef<(() => void)[]>([]);

  const [isListen, setIsListener] = useState(true);
  const onChangeLatest = useLatest(onChange);
  const schemaLatest = useLatest(schema);
  const cacheCustomVariableRefs = useRef<VariableRef[]>(
    schema?.customVariableRefs ?? [],
  );

  // Delete reference relationships that do not exist in the canvas
  const resetCustomVariableRefs = useCallback(
    ({ schema: _schema }: { schema: FabricSchema }) => {
      let newCustomVariableRefs = cacheCustomVariableRefs.current;

      const allObjectIds = _schema.objects.map(d => d.customId);
      newCustomVariableRefs = newCustomVariableRefs?.filter(d =>
        allObjectIds.includes(d.objectId),
      );
      cacheCustomVariableRefs.current = newCustomVariableRefs;

      return newCustomVariableRefs;
    },
    [],
  );

  // Monitor canvas changes
  useEffect(() => {
    if (canvas && onChangeLatest.current && isListen) {
      const _onChange = ({ isRemove }: { isRemove: boolean }) => {
        const json = canvas.toObject(saveProps) as FabricSchema;
        // When deleting, delete the invalid ref by the way.
        if (isRemove) {
          json.customVariableRefs = resetCustomVariableRefs({
            schema: json,
          });
        } else {
          json.customVariableRefs = cloneDeep(cacheCustomVariableRefs.current);
        }

        onChangeLatest.current?.(json);
      };

      eventDisposers.current.forEach(disposer => disposer());
      eventDisposers.current = [];

      listenerEvents.forEach(event => {
        const disposer = canvas.on(event as keyof CanvasEvents, function (e) {
          _onChange({
            isRemove: event === 'object:removed',
          });
        });
        eventDisposers.current.push(disposer);
      });
    }
    return () => {
      eventDisposers.current.forEach(disposer => disposer?.());
      eventDisposers.current = [];
    };
  }, [canvas, isListen]);

  /**
   * Generate new elements with references
   */
  const addRefObjectByVariable = useCallback(
    async (variable: InputVariable, element?: FabricObject) => {
      if (!canvas) {
        return;
      }
      const {
        customVariableRefs = [],
        width = 0,
        height = 0,
      } = schemaLatest.current ?? {};

      const { id, name, type } = variable;
      const centerXY = [
        width / 2 + customVariableRefs.length * 16,
        height / 2 + customVariableRefs.length * 16,
      ];

      let _element: FabricObject | undefined = element;

      // If no existing element is passed in, a new element is created
      if (!_element) {
        if (type === ViewVariableType.Image) {
          _element = await createElement({
            mode: Mode.IMAGE,
            position: [
              centerXY[0] - (defaultProps[Mode.IMAGE].width as number) / 2,
              centerXY[1] - (defaultProps[Mode.IMAGE].height as number) / 2,
            ],
            elementProps: {
              width: defaultProps[Mode.IMAGE].width,
              height: defaultProps[Mode.IMAGE].width,
              editable: false,
              src: ImagePlaceholder,
            },
          });
        } else if (type === ViewVariableType.String) {
          _element = await createElement({
            mode: Mode.BLOCK_TEXT,
            position: [
              centerXY[0] - (defaultProps[Mode.BLOCK_TEXT].width as number) / 2,
              centerXY[1] -
                (defaultProps[Mode.BLOCK_TEXT].height as number) / 2,
            ],
            elementProps: {
              text: I18n.t(
                'imageflow_canvas_change_text',
                {},
                '点击编辑文本预览',
              ),
              width: defaultProps[Mode.BLOCK_TEXT].width,
              height: defaultProps[Mode.BLOCK_TEXT].height,
            },
          });
        }
      }

      if (_element) {
        // Update reference relationship
        cacheCustomVariableRefs.current.push({
          variableId: id as string,
          objectId: (_element as FabricObjectWithCustomProps)
            .customId as string,
          variableName: name,
        });

        // Add to canvas and activate
        canvas.add(_element);
        canvas.setActiveObject(_element);
      }
    },
    [canvas],
  );

  /**
   * Update the reference relationship of the element specifying objectId
   * If the variable is empty, remove the reference
   * If variable is not empty & & customVariableRefs already has a correspondence, update the reference
   * If variable is not empty & & customVariableRefs does not have a correspondence, add a reference
   *
   */
  const updateRefByObjectId = useCallback(
    ({
      objectId,
      variable,
    }: {
      objectId: string;
      variable?: InputVariable;
    }) => {
      const customVariableRefs = cacheCustomVariableRefs.current;
      const targetRef = customVariableRefs.find(d => d.objectId === objectId);
      let newCustomVariableRefs = [];
      // If the variable is empty, remove the reference
      if (!variable) {
        newCustomVariableRefs = customVariableRefs.filter(
          d => d.objectId !== objectId,
        );
        // If variable is not empty & & customVariableRefs does not have a correspondence, add a reference
      } else if (!targetRef) {
        newCustomVariableRefs = [
          ...customVariableRefs,
          {
            variableId: variable.id as string,
            objectId,
            variableName: variable.name,
          },
        ];
        // If variable is not empty & & customVariableRefs already has a correspondence, update the reference
      } else {
        newCustomVariableRefs = customVariableRefs.map(d => {
          if (d.objectId === objectId) {
            return {
              ...d,
              variableId: variable.id as string,
              variableName: variable.name,
            };
          }
          return d;
        });
      }

      cacheCustomVariableRefs.current = newCustomVariableRefs;
      onChangeLatest.current?.({
        ...(schemaLatest.current as FabricSchema),
        customVariableRefs: newCustomVariableRefs,
      });
    },
    [onChangeLatest, schemaLatest],
  );

  /**
   * When variables change, update the variable names in the reference relationship
   */
  useEffect(() => {
    const { customVariableRefs = [] } = schemaLatest.current ?? {};
    const needsUpdate = customVariableRefs.some(ref => {
      const variable = variables?.find(v => v.id === ref.variableId);
      return ref.variableName !== (variable?.name ?? UNKNOWN_VARIABLE_NAME);
    });

    if (needsUpdate) {
      const newCustomVariableRefs = customVariableRefs.map(ref => {
        const variable = variables?.find(v => v.id === ref.variableId);
        return {
          ...ref,
          variableName: variable?.name ?? UNKNOWN_VARIABLE_NAME,
        };
      });

      cacheCustomVariableRefs.current = newCustomVariableRefs;
      onChangeLatest.current?.({
        ...(schemaLatest.current as FabricSchema),
        customVariableRefs: newCustomVariableRefs,
      });
    }
  }, [variables]);

  const stopListen = useCallback(() => {
    setIsListener(false);
  }, []);

  const startListen = useCallback(() => {
    setIsListener(true);
    // After redo undo, update the reference relationship
    cacheCustomVariableRefs.current =
      schemaLatest.current?.customVariableRefs ?? [];
  }, []);

  return {
    customVariableRefs: cacheCustomVariableRefs.current,
    addRefObjectByVariable,
    updateRefByObjectId,
    stopListen,
    startListen,
  };
};
