import type { z, ZodTypeDef } from 'zod';

import type { InlineEditor } from '../inline-editor.js';
import type { AttributeRenderer, InlineRange } from '../types.js';
import type { BaseTextAttributes } from '../utils/index.js';

import {
  baseTextAttributes,
  getDefaultAttributeRenderer,
} from '../utils/index.js';

export class AttributeService<TextAttributes extends BaseTextAttributes> {
  private _attributeRenderer: AttributeRenderer<TextAttributes> =
    getDefaultAttributeRenderer<TextAttributes>();

  private _attributeSchema: z.ZodSchema<TextAttributes, ZodTypeDef, unknown> =
    baseTextAttributes as z.ZodSchema<TextAttributes, ZodTypeDef, unknown>;

  private _marks: TextAttributes | null = null;

  getFormat = (inlineRange: InlineRange, loose = false): TextAttributes => {
    const deltas = this.editor.deltaService
      .getDeltasByInlineRange(inlineRange)
      .filter(([_, position]) => {
        const deltaStart = position.index;
        const deltaEnd = position.index + position.length;
        const inlineStart = inlineRange.index;
        const inlineEnd = inlineRange.index + inlineRange.length;

        if (inlineStart === inlineEnd) {
          return deltaStart < inlineStart && inlineStart <= deltaEnd;
        } else {
          return deltaEnd > inlineStart && deltaStart <= inlineEnd;
        }
      });
    const maybeAttributesList = deltas.map(([delta]) => delta.attributes);
    if (loose) {
      return maybeAttributesList.reduce(
        (acc, cur) => ({ ...acc, ...cur }),
        {}
      ) as TextAttributes;
    }
    if (
      !maybeAttributesList.length ||
      // some text does not have any attribute
      maybeAttributesList.some(attributes => !attributes)
    ) {
      return {} as TextAttributes;
    }
    const attributesList = maybeAttributesList as TextAttributes[];
    return attributesList.reduce((acc, cur) => {
      const newFormat = {} as TextAttributes;
      for (const key in acc) {
        const typedKey = key as keyof TextAttributes;
        // If the given range contains multiple different formats
        // such as links with different values,
        // we will treat it as having no format
        if (acc[typedKey] === cur[typedKey]) {
          // This cast is secure because we have checked that the value of the key is the same.
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          newFormat[typedKey] = acc[typedKey] as any;
        }
      }
      return newFormat;
    });
  };

  normalizeAttributes = (textAttributes?: TextAttributes) => {
    if (!textAttributes) {
      return undefined;
    }
    const attributeResult = this._attributeSchema.safeParse(textAttributes);
    if (!attributeResult.success) {
      console.error(attributeResult.error);
      return undefined;
    }
    return Object.fromEntries(
      // filter out undefined values
      Object.entries(attributeResult.data).filter(([_, v]) => v !== undefined)
    ) as TextAttributes;
  };

  resetMarks = (): void => {
    this._marks = null;
  };

  setAttributeRenderer = (renderer: AttributeRenderer<TextAttributes>) => {
    this._attributeRenderer = renderer;
  };

  setAttributeSchema = (
    schema: z.ZodSchema<TextAttributes, ZodTypeDef, unknown>
  ) => {
    this._attributeSchema = schema;
  };

  setMarks = (marks: TextAttributes): void => {
    this._marks = marks;
  };

  get attributeRenderer() {
    return this._attributeRenderer;
  }

  get marks() {
    return this._marks;
  }

  constructor(readonly editor: InlineEditor<TextAttributes>) {}
}
