import {
  DepthRenderer,
  Effect,
  PostProcess,
  RawTexture2DArray,
  RenderTargetTexture,
  Scene,
  SolidParticleSystem,
  Texture,
} from "@babylonjs/core";
import { Point3D } from "chootils/dist/points3d";
import { mover2dRefs, mover2dState, moverRefs, moverState } from "repond-movers";
import { MyTypes } from "../../declarations";
import { CustomVideoTexture } from "../../helpers/babylonjs/CustomVideoTexture";
import { InRangeForDoll } from "../../helpers/prendyUtils/dolls";
import { makeAutomaticMusicStartRefs, makeAutomaticSoundStartRefs } from "./globalStoreUtils";
import {
  AnyCameraName,
  AnySegmentName,
  CharacterName,
  DollName,
  ModelName,
  PickupName,
  PlaceInfoByName,
  PlaceName,
} from "../../types";

// save it to global
export type PrendySaveState = {
  global: {
    nowCamName: string;
    nowPlaceName: string;
    nowSegmentName: string;
    heldPickups: string[];
    storyOverlayToggled: boolean;
    alarmTextIsVisible: boolean;
    alarmText: string;
    aSpeechBubbleIsShowing: boolean;
    aConvoIsHappening: boolean;
  };
  dolls: Record<
    string,
    {
      position: Point3D;
      rotationY: number;
      isVisible: boolean;
      // collisionsEnabled: boolean; // setting isVisible also affects collisions
      toggledMeshes: Record<string, boolean>;
      inRange: Record<string, InRangeForDoll>;
      nowAnimation: string;
      // animWeightsGoal: Record<string, number>; // maybe not needed, since loading reacts to animation name
    }
  >;
  places: Record<
    string,
    {
      toggledWalls: Record<string, boolean>;
    }
  >;
  characters: Record<
    string,
    {
      hasLeftFirstTrigger: boolean;
    }
  >;
  player: {
    animationNames: {
      walking: string;
      idle: string;
    };
  };
  // (scraps) walkSpeed (move to state?)
  miniBubbles: Record<
    string,
    {
      isVisible: boolean;
      text: string;
      // forCharacter
    }
  >;
  speechBubbles: Record<
    string,
    {
      isVisible: boolean;
      goalText: string;
      // text?
      // visibleLetterAmount?
      nowVideoName: string;
      font: string;
      stylesBySpecialText: {
        [key: string]: {
          color: string;
          fontSize: string;
          fontWeight: string;
          fontStyle: string;
          textDecoration: string;
        };
      };
      forCharacter: string;
      typingFinished: boolean;
    }
  >;
  // (whole state)
  storyState: Record<any, any>;
};

export const timeStatePath = ["global", "main", "elapsedGameTime"] as const;

export default function global<T_MyTypes extends MyTypes = MyTypes>(prendyAssets: T_MyTypes["Assets"]) {
  const { musicNames, soundNames, placeInfoByName, prendyOptions } = prendyAssets;

  type MaybeSegmentName = null | AnySegmentName;
  type MaybeCam = null | AnyCameraName;

  type SegmentNameFromCameraAndPlace<
    T_Place extends keyof PlaceInfoByName,
    T_Cam extends keyof PlaceInfoByName[T_Place]["segmentNamesByCamera"]
  > = keyof PlaceInfoByName[T_Place]["segmentNamesByCamera"][T_Cam];

  type CameraNameFromPlace<T_Place extends keyof PlaceInfoByName> =
    keyof PlaceInfoByName[T_Place]["segmentNamesByCamera"];

  type CamSegmentRulesOptionsUntyped = Partial<{
    [P_PlaceName in PlaceName]: Partial<{
      [P_CamName in CameraNameFromPlace<P_PlaceName>]: (
        usefulStuff: Record<any, any> // usefulStoryStuff, but before the types for global state exist
      ) => SegmentNameFromCameraAndPlace<P_PlaceName, P_CamName>;
    }>;
  }>;

  const placeName = prendyOptions.place;

  // State
  const getDefaultState = () => ({
    // place
    nowPlaceName: placeName as PlaceName,
    goalPlaceName: null as null | PlaceName,
    readyToSwapPlace: false,
    isLoadingBetweenPlaces: true,
    loadingOverlayToggled: true,
    loadingOverlayFullyShowing: true,
    // cameras
    goalCamWhenNextPlaceLoads: null as MaybeCam,
    goalCamNameAtLoop: null as MaybeCam,
    goalCamName: null as MaybeCam, // NOTE always set goalCamName? and never nowCamName? to prepare everything first?
    nowCamName:
      ((prendyOptions.place === placeName ? prendyOptions.camera : "") ||
        ((placeInfoByName as any)?.[placeName as any]?.cameraNames?.[0] as unknown as AnyCameraName)) ??
      ("testItemCamName" as AnyCameraName), // if state() is called with a random itemId
    // segments and slice video
    nowSegmentName: prendyOptions.segment as AnySegmentName,
    goalSegmentName: null as MaybeSegmentName,
    goalSegmentNameAtLoop: null as MaybeSegmentName,
    goalSegmentWhenGoalPlaceLoads: null as MaybeSegmentName,
    // changing places
    modelNamesLoaded: [] as ModelName[],
    newPlaceModelLoaded: false,
    newPlaceVideosLoaded: false,
    newPlaceProbesLoaded: false,

    //
    // player
    playerCharacter: prendyOptions.playerCharacter as CharacterName, // TODO Move to players ?
    gravityValue: 5,
    playerMovingPaused: false, // to be able to prevent moving while theres a cutscene for example
    focusedDoll: prendyAssets.characterOptions[prendyOptions.playerCharacter].doll ?? ("walker" as DollName),
    focusedDollIsInView: false,
    //
    // slate
    ...mover2dState("slatePos"),
    ...moverState("slateZoom", {
      value: prendyOptions.zoomLevels.default,
      valueGoal: prendyOptions.zoomLevels.default,
      // springStopSpeed: 0.001, // NOTE not used in mover yet
    }), // (like scale)
    slatePosMoveConfigName: "default", // todo move to mover2dState()
    isOnVerticalScreen: false,
    zoomMultiplier: 1, // for vertical screens, zoom out a bit
    //
    // interacting
    timeScreenResized: Date.now(),
    interactButtonPressTime: 0,
    // story
    heldPickups: prendyOptions.heldPickups as PickupName[],
    storyOverlayToggled: false, // so the screen can fade out without affecting loading a new place
    alarmTextIsVisible: false, // TODO rename to title text?
    alarmText: "⚠ wobble detected ⚠", // TODO rename to title text?
    //
    // meta
    aSpeechBubbleIsShowing: false,
    aConvoIsHappening: false,
    //
    // backdrop times
    backdropTime: 0, // this is how far into the backdrop animation it is, it gets reset when reaching the end of a segment, and updated in the frame ticks
    backdropFrame: 0, // this is the current frame of the backdrop animation, based on the time and frame rate
    //
    frameTick: 0,
    timeMode: "game" as "game" | "pause" | "miniGame",
    elapsedGameTime: 0,
    elapsedPauseTime: 0,
    elapsedMiniGameTime: 0, // when not in the pause menu or the main game
    isGamePaused: false,
    gameTimeSpeed: prendyOptions.gameTimeSpeed,
    gameIsInBackground: false,
    //
    debugMessage: "",
    //
    latestSave: null as null | PrendySaveState,
    latestLoadTime: 0, // so things can be initialed after loading state, like isVisible
  });

  // Refs
  const getDefaultRefs = () => ({
    scene: null as null | Scene,
    backdropTex: null as null | Texture,
    backdropTexDepth: null as null | Texture,
    depthRenderer: null as DepthRenderer | null,
    depthRenderTarget: null as null | RenderTargetTexture,
    backdropPostProcess: null as null | PostProcess,
    backdropPostProcessEffect: null as null | Effect,
    fxaaPostProcess: null as null | PostProcess,
    // Video shader stuff
    stretchVideoSize: { x: 1, y: 1 },
    stretchVideoGoalSize: { x: 1, y: 1 },
    stretchSceneSize: { x: 1, y: 1 },
    //
    ...mover2dRefs("slatePos", { mass: 41.5, stiffness: 50, damping: 10, friction: 0.35, stopSpeed: 0.003 }),
    ...moverRefs("slateZoom", { mass: 41.5, stiffness: 25, damping: 10, friction: 0.35 }), // NOTE stopSpeed not on 1dMover
    //
    sounds: makeAutomaticSoundStartRefs(soundNames),
    music: makeAutomaticMusicStartRefs(musicNames),
    musicEffects: {
      lowPass: null as null | BiquadFilterNode,
      compress: null as null | DynamicsCompressorNode,
      extraGain: null as null | GainNode,
    },
    //
    solidParticleSystems: {} as Record<string, SolidParticleSystem>,
    //
    // Temporary
    timerSpeed: 1,
    //
    aConvoIsHappening_timeout: null as null | ReturnType<typeof setTimeout>,
    //
    camSegmentRulesOptions: null as null | CamSegmentRulesOptionsUntyped, // NOTE if using the typed version,  might need to define it in consts to remove cyclic dependancy
    // onPickupButtonClick: null as null | ((pickupName: PickupName) => void), // what to do when pressing the pickup button
    onPickupButtonClick: null as null | ((pickupName: any) => void), // what to do when pressing the pickup button
  });

  // const startStates: InitialItemsState<typeof state> = {
  const startStates = {
    main: getDefaultState(),
  };

  return { startStates, getDefaultState, getDefaultRefs };
}
