import type {BlockOffset, Editor, EditorSchema} from '@portabletext/editor'
import {useEditor} from '@portabletext/editor'
import {
  defineBehavior,
  effect,
  execute,
  forward,
} from '@portabletext/editor/behaviors'
import * as utils from '@portabletext/editor/utils'
import {useActorRef} from '@xstate/react'
import {isDeepEqual} from 'remeda'
import {
  assign,
  fromCallback,
  setup,
  type AnyEventObject,
  type CallbackLogicFunction,
} from 'xstate'
import {createCharacterPairDecoratorBehavior} from './behavior.character-pair-decorator'

/**
 * @beta
 */
export function CharacterPairDecoratorPlugin(config: {
  decorator: ({schema}: {schema: EditorSchema}) => string | undefined
  pair: {char: string; amount: number}
}) {
  const editor = useEditor()

  useActorRef(decoratorPairMachine, {
    input: {
      editor,
      decorator: config.decorator,
      pair: config.pair,
    },
  })

  return null
}

type DecoratorPairEvent =
  | {
      type: 'decorator.add'
      blockOffset: BlockOffset
    }
  | {
      type: 'selection'
      blockOffsets?: {
        anchor: BlockOffset
        focus: BlockOffset
      }
    }
  | {
      type: 'delete.backward'
    }

const decorateListener: CallbackLogicFunction<
  AnyEventObject,
  DecoratorPairEvent,
  {
    decorator: ({schema}: {schema: EditorSchema}) => string | undefined
    editor: Editor
    pair: {char: string; amount: number}
  }
> = ({sendBack, input}) => {
  const unregister = input.editor.registerBehavior({
    behavior: createCharacterPairDecoratorBehavior({
      decorator: input.decorator,
      pair: input.pair,
      onDecorate: (offset) => {
        sendBack({type: 'decorator.add', blockOffset: offset})
      },
    }),
  })

  return unregister
}

const selectionListenerCallback: CallbackLogicFunction<
  AnyEventObject,
  DecoratorPairEvent,
  {editor: Editor}
> = ({sendBack, input}) => {
  const unregister = input.editor.registerBehavior({
    behavior: defineBehavior({
      on: 'select',
      guard: ({snapshot, event}) => {
        if (!event.at) {
          return {blockOffsets: undefined}
        }

        const anchor = utils.spanSelectionPointToBlockOffset({
          context: snapshot.context,
          selectionPoint: event.at.anchor,
        })
        const focus = utils.spanSelectionPointToBlockOffset({
          context: snapshot.context,
          selectionPoint: event.at.focus,
        })

        if (!anchor || !focus) {
          return {blockOffsets: undefined}
        }

        return {
          blockOffsets: {
            anchor,
            focus,
          },
        }
      },
      actions: [
        ({event}, {blockOffsets}) => [
          {
            type: 'effect',
            effect: () => {
              sendBack({type: 'selection', blockOffsets})
            },
          },
          forward(event),
        ],
      ],
    }),
  })

  return unregister
}

const deleteBackwardListenerCallback: CallbackLogicFunction<
  AnyEventObject,
  DecoratorPairEvent,
  {editor: Editor}
> = ({sendBack, input}) => {
  const unregister = input.editor.registerBehavior({
    behavior: defineBehavior({
      on: 'delete.backward',
      actions: [
        () => [
          execute({
            type: 'history.undo',
          }),
          effect(() => {
            sendBack({type: 'delete.backward'})
          }),
        ],
      ],
    }),
  })

  return unregister
}

const decoratorPairMachine = setup({
  types: {
    context: {} as {
      decorator: ({schema}: {schema: EditorSchema}) => string | undefined
      editor: Editor
      offsetAfterDecorator?: BlockOffset
      pair: {char: string; amount: number}
    },
    input: {} as {
      decorator: ({schema}: {schema: EditorSchema}) => string | undefined
      editor: Editor
      pair: {char: string; amount: number}
    },
    events: {} as DecoratorPairEvent,
  },
  actors: {
    'decorate listener': fromCallback(decorateListener),
    'delete.backward listener': fromCallback(deleteBackwardListenerCallback),
    'selection listener': fromCallback(selectionListenerCallback),
  },
}).createMachine({
  id: 'decorator pair',
  context: ({input}) => ({
    decorator: input.decorator,
    editor: input.editor,
    pair: input.pair,
  }),
  initial: 'idle',
  states: {
    'idle': {
      invoke: [
        {
          src: 'decorate listener',
          input: ({context}) => ({
            decorator: context.decorator,
            editor: context.editor,
            pair: context.pair,
          }),
        },
      ],
      on: {
        'decorator.add': {
          target: 'decorator added',
          actions: assign({
            offsetAfterDecorator: ({event}) => event.blockOffset,
          }),
        },
      },
    },
    'decorator added': {
      exit: [
        assign({
          offsetAfterDecorator: undefined,
        }),
      ],
      invoke: [
        {
          src: 'selection listener',
          input: ({context}) => ({editor: context.editor}),
        },
        {
          src: 'delete.backward listener',
          input: ({context}) => ({editor: context.editor}),
        },
      ],
      on: {
        'selection': {
          target: 'idle',
          guard: ({context, event}) => {
            const selectionChanged = !isDeepEqual(
              {
                anchor: context.offsetAfterDecorator,
                focus: context.offsetAfterDecorator,
              },
              event.blockOffsets,
            )

            return selectionChanged
          },
        },
        'delete.backward': {
          target: 'idle',
        },
      },
    },
  },
})
