import { createOpaqueId } from "../core/ids";
import {
  areControlEffectsEqual,
  cloneControlEffect,
  createModelInstanceControlTargetRef,
  createPlayModelAnimationControlEffect,
  createPlaySoundControlEffect,
  createSoundEmitterControlTargetRef,
  createStopModelAnimationControlEffect,
  createStopSoundControlEffect,
  getControlEffectLabel,
  type ControlEffect
} from "../controls/control-surface";

export const INTERACTION_TRIGGER_KINDS = ["enter", "exit", "click"] as const;
export type InteractionTriggerKind = (typeof INTERACTION_TRIGGER_KINDS)[number];

export interface TeleportPlayerAction {
  type: "teleportPlayer";
  targetEntityId: string;
}

export interface ToggleVisibilityAction {
  type: "toggleVisibility";
  targetBrushId: string;
  visible?: boolean;
}

export interface PlayAnimationAction {
  type: "playAnimation";
  targetModelInstanceId: string;
  clipName: string;
  loop?: boolean;
}

export interface StopAnimationAction {
  type: "stopAnimation";
  targetModelInstanceId: string;
}

export interface PlaySoundAction {
  type: "playSound";
  targetSoundEmitterId: string;
}

export interface StopSoundAction {
  type: "stopSound";
  targetSoundEmitterId: string;
}

export interface RunSequenceAction {
  type: "runSequence";
  sequenceId: string;
}

export interface ControlInteractionAction {
  type: "control";
  effect: ControlEffect;
}

export type InteractionAction =
  | TeleportPlayerAction
  | ToggleVisibilityAction
  | PlayAnimationAction
  | StopAnimationAction
  | PlaySoundAction
  | StopSoundAction
  | RunSequenceAction
  | ControlInteractionAction;

export interface InteractionLink {
  id: string;
  sourceEntityId: string;
  trigger: InteractionTriggerKind;
  action: InteractionAction;
}

export interface CreateTeleportPlayerInteractionLinkOptions {
  id?: string;
  sourceEntityId: string;
  trigger?: InteractionTriggerKind;
  targetEntityId: string;
}

export interface CreateToggleVisibilityInteractionLinkOptions {
  id?: string;
  sourceEntityId: string;
  trigger?: InteractionTriggerKind;
  targetBrushId: string;
  visible?: boolean;
}

export interface CreatePlayAnimationInteractionLinkOptions {
  id?: string;
  sourceEntityId: string;
  trigger?: InteractionTriggerKind;
  targetModelInstanceId: string;
  clipName: string;
  loop?: boolean;
}

export interface CreateStopAnimationInteractionLinkOptions {
  id?: string;
  sourceEntityId: string;
  trigger?: InteractionTriggerKind;
  targetModelInstanceId: string;
}

export interface CreatePlaySoundInteractionLinkOptions {
  id?: string;
  sourceEntityId: string;
  trigger?: InteractionTriggerKind;
  targetSoundEmitterId: string;
}

export interface CreateStopSoundInteractionLinkOptions {
  id?: string;
  sourceEntityId: string;
  trigger?: InteractionTriggerKind;
  targetSoundEmitterId: string;
}

export interface CreateControlInteractionLinkOptions {
  id?: string;
  sourceEntityId: string;
  trigger?: InteractionTriggerKind;
  effect: ControlEffect;
}

export interface CreateRunSequenceInteractionLinkOptions {
  id?: string;
  sourceEntityId: string;
  trigger?: InteractionTriggerKind;
  sequenceId: string;
}

function assertNonEmptyString(value: string, label: string) {
  if (value.trim().length === 0) {
    throw new Error(`${label} must be non-empty.`);
  }
}

function cloneAction(action: InteractionAction): InteractionAction {
  switch (action.type) {
    case "teleportPlayer":
      return {
        type: "teleportPlayer",
        targetEntityId: action.targetEntityId
      };
    case "toggleVisibility":
      return {
        type: "toggleVisibility",
        targetBrushId: action.targetBrushId,
        visible: action.visible
      };
    case "playAnimation":
      return {
        type: "playAnimation",
        targetModelInstanceId: action.targetModelInstanceId,
        clipName: action.clipName,
        loop: action.loop
      };
    case "stopAnimation":
      return {
        type: "stopAnimation",
        targetModelInstanceId: action.targetModelInstanceId
      };
    case "playSound":
      return {
        type: "playSound",
        targetSoundEmitterId: action.targetSoundEmitterId
      };
    case "stopSound":
      return {
        type: "stopSound",
        targetSoundEmitterId: action.targetSoundEmitterId
      };
    case "runSequence":
      return {
        type: "runSequence",
        sequenceId: action.sequenceId
      };
    case "control":
      return {
        type: "control",
        effect: cloneControlEffect(action.effect)
      };
  }
}

export function isInteractionTriggerKind(
  value: unknown
): value is InteractionTriggerKind {
  return value === "enter" || value === "exit" || value === "click";
}

export function isControlInteractionAction(
  action: InteractionAction
): action is ControlInteractionAction {
  return action.type === "control";
}

export function createTeleportPlayerInteractionLink(
  options: CreateTeleportPlayerInteractionLinkOptions
): InteractionLink {
  assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
  assertNonEmptyString(options.targetEntityId, "Teleport target entity id");

  return {
    id: options.id ?? createOpaqueId("interaction-link"),
    sourceEntityId: options.sourceEntityId,
    trigger: options.trigger ?? "enter",
    action: {
      type: "teleportPlayer",
      targetEntityId: options.targetEntityId
    }
  };
}

export function createToggleVisibilityInteractionLink(
  options: CreateToggleVisibilityInteractionLinkOptions
): InteractionLink {
  assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
  assertNonEmptyString(options.targetBrushId, "Visibility target brush id");

  if (options.visible !== undefined && typeof options.visible !== "boolean") {
    throw new Error(
      "Visibility action visible must be a boolean when authored."
    );
  }

  return {
    id: options.id ?? createOpaqueId("interaction-link"),
    sourceEntityId: options.sourceEntityId,
    trigger: options.trigger ?? "enter",
    action: {
      type: "toggleVisibility",
      targetBrushId: options.targetBrushId,
      visible: options.visible
    }
  };
}

export function createPlayAnimationInteractionLink(
  options: CreatePlayAnimationInteractionLinkOptions
): InteractionLink {
  assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
  assertNonEmptyString(
    options.targetModelInstanceId,
    "Play animation target model instance id"
  );
  assertNonEmptyString(options.clipName, "Play animation clip name");

  return {
    id: options.id ?? createOpaqueId("interaction-link"),
    sourceEntityId: options.sourceEntityId,
    trigger: options.trigger ?? "enter",
    action: {
      type: "playAnimation",
      targetModelInstanceId: options.targetModelInstanceId,
      clipName: options.clipName,
      loop: options.loop
    }
  };
}

export function createStopAnimationInteractionLink(
  options: CreateStopAnimationInteractionLinkOptions
): InteractionLink {
  assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
  assertNonEmptyString(
    options.targetModelInstanceId,
    "Stop animation target model instance id"
  );

  return {
    id: options.id ?? createOpaqueId("interaction-link"),
    sourceEntityId: options.sourceEntityId,
    trigger: options.trigger ?? "enter",
    action: {
      type: "stopAnimation",
      targetModelInstanceId: options.targetModelInstanceId
    }
  };
}

export function createPlaySoundInteractionLink(
  options: CreatePlaySoundInteractionLinkOptions
): InteractionLink {
  assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
  assertNonEmptyString(
    options.targetSoundEmitterId,
    "Play sound target sound emitter id"
  );

  return {
    id: options.id ?? createOpaqueId("interaction-link"),
    sourceEntityId: options.sourceEntityId,
    trigger: options.trigger ?? "enter",
    action: {
      type: "playSound",
      targetSoundEmitterId: options.targetSoundEmitterId
    }
  };
}

export function createStopSoundInteractionLink(
  options: CreateStopSoundInteractionLinkOptions
): InteractionLink {
  assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
  assertNonEmptyString(
    options.targetSoundEmitterId,
    "Stop sound target sound emitter id"
  );

  return {
    id: options.id ?? createOpaqueId("interaction-link"),
    sourceEntityId: options.sourceEntityId,
    trigger: options.trigger ?? "enter",
    action: {
      type: "stopSound",
      targetSoundEmitterId: options.targetSoundEmitterId
    }
  };
}

export function createControlInteractionLink(
  options: CreateControlInteractionLinkOptions
): InteractionLink {
  assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");

  return {
    id: options.id ?? createOpaqueId("interaction-link"),
    sourceEntityId: options.sourceEntityId,
    trigger: options.trigger ?? "enter",
    action: {
      type: "control",
      effect: cloneControlEffect(options.effect)
    }
  };
}

export function createRunSequenceInteractionLink(
  options: CreateRunSequenceInteractionLinkOptions
): InteractionLink {
  assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
  assertNonEmptyString(options.sequenceId, "Sequence id");

  return {
    id: options.id ?? createOpaqueId("interaction-link"),
    sourceEntityId: options.sourceEntityId,
    trigger: options.trigger ?? "enter",
    action: {
      type: "runSequence",
      sequenceId: options.sequenceId
    }
  };
}

export function getInteractionActionControlEffect(
  action: InteractionAction
): ControlEffect | null {
  switch (action.type) {
    case "playAnimation":
      return createPlayModelAnimationControlEffect({
        target: createModelInstanceControlTargetRef(
          action.targetModelInstanceId
        ),
        clipName: action.clipName,
        loop: action.loop
      });
    case "stopAnimation":
      return createStopModelAnimationControlEffect({
        target: createModelInstanceControlTargetRef(
          action.targetModelInstanceId
        )
      });
    case "playSound":
      return createPlaySoundControlEffect({
        target: createSoundEmitterControlTargetRef(action.targetSoundEmitterId)
      });
    case "stopSound":
      return createStopSoundControlEffect({
        target: createSoundEmitterControlTargetRef(action.targetSoundEmitterId)
      });
    case "runSequence":
      return null;
    case "control":
      return cloneControlEffect(action.effect);
    case "teleportPlayer":
    case "toggleVisibility":
      return null;
  }
}

export function getInteractionActionLabel(action: InteractionAction): string {
  switch (action.type) {
    case "teleportPlayer":
      return "Teleport Player";
    case "toggleVisibility":
      return "Toggle Visibility";
    case "playAnimation":
      return "Play Animation";
    case "stopAnimation":
      return "Stop Animation";
    case "playSound":
      return "Play Sound";
    case "stopSound":
      return "Stop Sound";
    case "runSequence":
      return "Run Sequence";
    case "control":
      return getControlEffectLabel(action.effect);
  }
}

export function cloneInteractionLink(link: InteractionLink): InteractionLink {
  return {
    id: link.id,
    sourceEntityId: link.sourceEntityId,
    trigger: link.trigger,
    action: cloneAction(link.action)
  };
}

export function areInteractionLinksEqual(
  left: InteractionLink,
  right: InteractionLink
): boolean {
  if (
    left.id !== right.id ||
    left.sourceEntityId !== right.sourceEntityId ||
    left.trigger !== right.trigger
  ) {
    return false;
  }

  if (left.action.type !== right.action.type) {
    return false;
  }

  switch (left.action.type) {
    case "teleportPlayer":
      return (
        left.action.targetEntityId ===
        (right.action as TeleportPlayerAction).targetEntityId
      );
    case "toggleVisibility":
      return (
        left.action.targetBrushId ===
          (right.action as ToggleVisibilityAction).targetBrushId &&
        left.action.visible === (right.action as ToggleVisibilityAction).visible
      );
    case "playAnimation":
      return (
        left.action.targetModelInstanceId ===
          (right.action as PlayAnimationAction).targetModelInstanceId &&
        left.action.clipName ===
          (right.action as PlayAnimationAction).clipName &&
        left.action.loop === (right.action as PlayAnimationAction).loop
      );
    case "stopAnimation":
      return (
        left.action.targetModelInstanceId ===
        (right.action as StopAnimationAction).targetModelInstanceId
      );
    case "playSound":
      return (
        left.action.targetSoundEmitterId ===
        (right.action as PlaySoundAction).targetSoundEmitterId
      );
    case "stopSound":
      return (
        left.action.targetSoundEmitterId ===
        (right.action as StopSoundAction).targetSoundEmitterId
      );
    case "runSequence":
      return (
        left.action.sequenceId === (right.action as RunSequenceAction).sequenceId
      );
    case "control":
      return areControlEffectsEqual(
        left.action.effect,
        (right.action as ControlInteractionAction).effect
      );
  }
}

export function cloneInteractionLinkRegistry(
  links: Record<string, InteractionLink>
): Record<string, InteractionLink> {
  return Object.fromEntries(
    Object.entries(links).map(([linkId, link]) => [
      linkId,
      cloneInteractionLink(link)
    ])
  );
}

export function compareInteractionLinks(
  left: InteractionLink,
  right: InteractionLink
): number {
  if (left.sourceEntityId !== right.sourceEntityId) {
    return left.sourceEntityId.localeCompare(right.sourceEntityId);
  }

  if (left.trigger !== right.trigger) {
    return left.trigger.localeCompare(right.trigger);
  }

  return left.id.localeCompare(right.id);
}

export function getInteractionLinks(
  links: Record<string, InteractionLink>
): InteractionLink[] {
  return Object.values(links).sort(compareInteractionLinks);
}

export function getInteractionLinksForSource(
  links: Record<string, InteractionLink>,
  sourceEntityId: string
): InteractionLink[] {
  return getInteractionLinks(links).filter(
    (link) => link.sourceEntityId === sourceEntityId
  );
}
