export type VQSpeechOptions<Voice = unknown> = Readonly<{
  subtitle?: string;
  readAs?: string;
  voicevox?: Partial<Voice>;
  durationSeconds?: number;
}>;

export type VQSpeechEvent<
  CharacterId extends string = string,
  Voice = unknown
> = Readonly<{
  type: "say";
  id: string;
  character: CharacterId;
  text: string;
  subtitle?: string;
  readAs?: string;
  voicevox?: Partial<Voice>;
  durationSeconds?: number;
}>;

export type VQStillObjectFit =
  | "cover"
  | "contain"
  | "fill"
  | "none"
  | "scale-down";

export type VQStillEvent = Readonly<{
  type: "still";
  id: string;
  imagePath: string;
  durationSeconds?: number;
  fit?: VQStillObjectFit;
  objectPosition?: string;
  opacity?: number;
}>;

export type VQWaitEvent = Readonly<{
  type: "wait";
  id: string;
  durationSeconds: number;
}>;

export type VQWaitEventInput = Readonly<{
  type: "wait";
  durationSeconds: number;
}>;

export type VQVideoPlacement = "blocking" | "overlay" | "background";

export type VQVideoPlayback = "once" | "loop";

export type VQVideoEvent = Readonly<{
  type: "video";
  id: string;
  src: string;
  placement?: VQVideoPlacement;
  playback?: VQVideoPlayback;
  durationFrames?: number;
  durationSeconds?: number;
  trimBeforeFrames?: number;
  trimAfterFrames?: number;
  muted?: boolean;
  volume?: number;
  playbackRate?: number;
  toneFrequency?: number;
  loopVolumeCurveBehavior?: "repeat" | "extend";
  fit?: VQStillObjectFit;
  objectPosition?: string;
  opacity?: number;
  zIndex?: number;
}>;

export type VQCustomTimelineEvent<
  Type extends string,
  Payload extends object = object
> = Readonly<
  {
    type: Type;
  } & Payload
>;

export type VQTimelineEvent<
  CharacterId extends string = string,
  Voice = unknown,
  ExtraEvent extends Readonly<{type: string}> = never
> =
  | VQSpeechEvent<CharacterId, Voice>
  | VQStillEvent
  | VQWaitEvent
  | VQVideoEvent
  | ExtraEvent;

export type VQTimelineInputEvent<
  CharacterId extends string = string,
  Voice = unknown,
  ExtraEvent extends Readonly<{type: string}> = never
> =
  | VQSpeechEvent<CharacterId, Voice>
  | VQStillEvent
  | VQWaitEvent
  | VQWaitEventInput
  | VQVideoEvent
  | ExtraEvent;

type VQTimelineInputEventBase = Readonly<{type: string}>;

export type VQNormalizeTimelineEvent<Event> =
  Event extends Readonly<{type: "wait"; durationSeconds: infer Duration}>
    ? Readonly<Omit<Event, "id"> & {id: string; durationSeconds: Duration}>
    : Event;

export type VQDefinedTimeline<
  Timeline extends readonly VQTimelineInputEventBase[]
> = {
  readonly [Index in keyof Timeline]: VQNormalizeTimelineEvent<Timeline[Index]>;
};

// 用途: VOICEVOX発話をタイムラインイベントとして定義する。
// 使用方法: script.ts の timeline 内で say("id", "character", "text", options) として呼び出す。
// オプションや引数詳細: options には字幕・読み上げ用テキスト・VOICEVOX上書き・音声未生成時の秒数を指定できる。
export const say = <CharacterId extends string, Voice = unknown>(
  id: string,
  character: CharacterId,
  text: string,
  options: VQSpeechOptions<Voice> = {}
): VQSpeechEvent<CharacterId, Voice> => ({
  type: "say",
  id,
  character,
  text,
  ...options,
});

// 用途: 静止画をタイムラインイベントとして定義する。
// 使用方法: script.ts の timeline 内で still("id", "path", options) として呼び出す。
// オプションや引数詳細: options には表示秒数・object-fit・表示位置・透明度を指定できる。
export const still = (
  id: string,
  imagePath: string,
  options: Omit<VQStillEvent, "type" | "id" | "imagePath"> = {}
): VQStillEvent => ({
  type: "still",
  id,
  imagePath,
  ...options,
});

// 用途: 何も発話しない待機時間をタイムラインイベントとして定義する。
// 使用方法: script.ts の timeline 内で wait(1) のように秒数だけを指定し、defineVQTimelineでIDを確定する。
// オプションや引数詳細: durationSeconds は待機秒数で、IDはtimeline単位で wait-001 から自動採番される。
export const wait = (durationSeconds: number): VQWaitEventInput => ({
  type: "wait",
  durationSeconds,
});

// 用途: 動画をタイムラインイベントとして定義する。
// 使用方法: script.ts の timeline 内で video("id", "path.mp4", options) として呼び出す。
// オプションや引数詳細: placement はタイムライン進行、playback は単発/ループ、duration は表示尺を指定する。
export const video = (
  id: string,
  src: string,
  options: Omit<VQVideoEvent, "type" | "id" | "src"> = {}
): VQVideoEvent => ({
  type: "video",
  id,
  src,
  ...options,
});

// 用途: 共通タイムラインイベントが時間カーソルを進めるかを判定する。
// 使用方法: scheduleVQChronologicalScenarioのdoesEventAdvanceTimelineに渡して使う。
// オプションや引数詳細: videoのoverlay/backgroundは現在位置で再生を始め、次イベントを同時に開始できる。
export const doesVQTimelineEventAdvanceTimeline = (
  event: Readonly<{type: string; placement?: string}>
) =>
  event.type !== "video" ||
  event.placement === undefined ||
  event.placement === "blocking";

// 用途: waitイベントの連番からタイムライン内IDを作る。
// 使用方法: defineVQTimeline内で未解決waitイベントを正規化するときに呼び出す。
// オプションや引数詳細: index は1始まりのwait連番で、3桁ゼロ埋めにする。
const waitEventId = (index: number) =>
  `wait-${String(index).padStart(3, "0")}`;

// 用途: タイムライン配列を確定し、未解決のwaitイベントへ安定したIDを付与する。
// 使用方法: script.ts で defineVQTimeline([say(...), wait(1)]) の形でtimelineを定義する。
// オプションや引数詳細: wait IDはこの関数に渡された配列の中だけで先頭から採番される。
export const defineVQTimeline = <
  const Timeline extends readonly VQTimelineInputEventBase[]
>(
  timeline: Timeline
): VQDefinedTimeline<Timeline> => {
  let waitEventIndex = 0;

  return timeline.map((event) => {
    if (event.type === "wait") {
      waitEventIndex += 1;

      if (!("id" in event)) {
        return {
          ...event,
          id: waitEventId(waitEventIndex),
        };
      }
    }

    return event;
  }) as VQDefinedTimeline<Timeline>;
};

// 用途: タイムラインイベントから発話イベントだけを型安全に判定する。
// 使用方法: event.type を直接比較したくない場所で isVQSpeechEvent(event) として使う。
// オプションや引数詳細: CharacterId・Voice・拡張イベント型は呼び出し元のTimelineEventに合わせて推論される。
export const isVQSpeechEvent = <
  CharacterId extends string = string,
  Voice = unknown,
  ExtraEvent extends Readonly<{type: string}> = never
>(
  event: VQTimelineEvent<CharacterId, Voice, ExtraEvent>
): event is VQSpeechEvent<CharacterId, Voice> => event.type === "say";
