import { createSlice, PayloadAction, createSelector } from '@reduxjs/toolkit';
import { store, RootState } from '../../app/store';
import { Matrix } from '../../util/matrix';
import { Transform3D } from '../../util/transform3d';
import { round } from '../../util/round';

export enum MovementMode {
  ORBIT,
  TRANSLATE,
  ROTATE,
}

interface Point3D {
  x: number;
  y: number;
  z: number;
}

interface Orientation3D {
  x: number;
  y: number;
  z: number;
}

interface Color {
  r: number;
  g: number;
  b: number;
}

interface RendererOptions {
  camera: {
    position: Point3D;
    towards: Point3D;
    projection: {
      vfov: number;
      near: number;
      far: number;
    };
    followers: {
      cameraTowards: boolean;
      lightSourcePosition: boolean;
    };
    movement: MovementMode;
  };
  object: {
    position: Point3D;
    orientation: Orientation3D;
    color: Color;
  };
  lightSource: {
    position: Point3D;
  };
  resetTo: Omit<RendererOptions, 'resetTo'>;
}

const partialInitialState: Omit<RendererOptions, 'resetTo'> = {
  camera: {
    position: { x: 0, y: 0, z: 0 },
    towards: { x: 0, y: 0, z: 1 },
    projection: {
      vfov: 70,
      near: 1,
      far: 100,
    },
    followers: {
      cameraTowards: true,
      lightSourcePosition: true,
    },
    movement: MovementMode.ORBIT,
  },
  object: {
    position: { x: 0, y: 0, z: 0 },
    orientation: { x: 0, y: 0, z: 0 },
    color: { r: 128, g: 128, b: 128 },
  },
  lightSource: {
    position: { x: 0, y: 0, z: 0 },
  },
};
const initialState: RendererOptions = {
  ...partialInitialState,
  resetTo: partialInitialState,
};

export const rendererSlice = createSlice({
  name: 'renderer',
  initialState,
  reducers: {
    moveCamera: {
      prepare: (x: number, y: number, z: number) => ({ payload: { x, y, z } }),
      reducer: (state, { payload: { x, y, z } }: PayloadAction<Point3D>) => {
        switch (state.camera.movement) {
          case MovementMode.ORBIT: {
            const targetViewEye = new Matrix([
              state.camera.towards.x,
              state.camera.towards.y,
              state.camera.towards.z,
            ]);
            const targetViewAt = new Matrix([
              state.camera.towards.x - state.camera.position.x,
              state.camera.towards.y - state.camera.position.y,
              state.camera.towards.z - state.camera.position.z,
            ]).add(targetViewEye);
            const targetViewTransform = Transform3D.lookAt(
              targetViewEye,
              targetViewAt
            );
            const targetViewInverseTransform = Transform3D.inverseLookAt(
              targetViewTransform
            );
            const orbitTransform = targetViewTransform
              .multiply(Transform3D.rotateY(x / 10))
              .multiply(Transform3D.rotateX(y / 10))
              .multiply(Transform3D.scale(1 - z / 400))
              .multiply(targetViewInverseTransform);

            const newCameraPosition = new Matrix([
              state.camera.position.x,
              state.camera.position.y,
              state.camera.position.z,
              1,
            ]).multiply(orbitTransform);
            state.camera.position.x = newCameraPosition.at(0);
            state.camera.position.y = newCameraPosition.at(1);
            state.camera.position.z = newCameraPosition.at(2);

            if (state.camera.followers.lightSourcePosition) {
              const newLightSourcePosition = new Matrix([
                state.lightSource.position.x,
                state.lightSource.position.y,
                state.lightSource.position.z,
                1,
              ]).multiply(orbitTransform);
              state.lightSource.position.x = newLightSourcePosition.at(0);
              state.lightSource.position.y = newLightSourcePosition.at(1);
              state.lightSource.position.z = newLightSourcePosition.at(2);
            }
            break;
          }

          case MovementMode.TRANSLATE: {
            const translation = new Matrix([
              -x / 100,
              y / 100,
              z / 100,
              0,
            ]).multiply(
              Transform3D.inverseLookAt(
                Transform3D.lookAt(
                  new Matrix([
                    state.camera.position.x,
                    state.camera.position.y,
                    state.camera.position.z,
                  ]),
                  new Matrix([
                    state.camera.towards.x,
                    state.camera.towards.y,
                    state.camera.towards.z,
                  ])
                )
              )
            );
            const translationX = translation.at(0);
            const translationY = translation.at(1);
            const translationZ = translation.at(2);

            state.camera.position.x += translationX;
            state.camera.position.y += translationY;
            state.camera.position.z += translationZ;
            if (state.camera.followers.cameraTowards) {
              state.camera.towards.x += translationX;
              state.camera.towards.y += translationY;
              state.camera.towards.z += translationZ;
            }
            if (state.camera.followers.lightSourcePosition) {
              state.lightSource.position.x += translationX;
              state.lightSource.position.y += translationY;
              state.lightSource.position.z += translationZ;
            }
            break;
          }

          case MovementMode.ROTATE: {
            const targetViewTransform = Transform3D.lookAt(
              new Matrix([
                state.camera.position.x,
                state.camera.position.y,
                state.camera.position.z,
              ]),
              new Matrix([
                state.camera.towards.x,
                state.camera.towards.y,
                state.camera.towards.z,
              ])
            );
            const targetViewInverseTransform = Transform3D.inverseLookAt(
              targetViewTransform
            );
            const rotateTransform = targetViewTransform
              .multiply(Transform3D.rotateY(-x / 10))
              .multiply(Transform3D.rotateX(-y / 10))
              .multiply(targetViewInverseTransform);
            const newCameraTowards = new Matrix([
              state.camera.towards.x,
              state.camera.towards.y,
              state.camera.towards.z,
              1,
            ]).multiply(rotateTransform);
            state.camera.towards.x = newCameraTowards.at(0);
            state.camera.towards.y = newCameraTowards.at(1);
            state.camera.towards.z = newCameraTowards.at(2);
            break;
          }
        }
      },
    },
    zoomIn: (state) => {
      const proposed = state.camera.projection.vfov - 5;
      if (proposed > 0) {
        state.camera.projection.vfov = proposed;
      }
    },
    zoomOut: (state) => {
      const proposed = state.camera.projection.vfov + 5;
      if (proposed < 180) {
        state.camera.projection.vfov = proposed;
      }
    },
    setCameraPosition: {
      prepare: (x: number, y: number, z: number) => ({ payload: { x, y, z } }),
      reducer: (state, { payload }: PayloadAction<Point3D>) => {
        const deltaX = payload.x - state.camera.position.x;
        const deltaY = payload.y - state.camera.position.y;
        const deltaZ = payload.z - state.camera.position.z;
        state.camera.position = payload;
        if (state.camera.followers.cameraTowards) {
          state.camera.towards.x += deltaX;
          state.camera.towards.y += deltaY;
          state.camera.towards.z += deltaZ;
        }
        if (state.camera.followers.lightSourcePosition) {
          state.lightSource.position.x += deltaX;
          state.lightSource.position.y += deltaY;
          state.lightSource.position.z += deltaZ;
        }
      },
    },
    setCameraTowards: {
      prepare: (x: number, y: number, z: number) => ({ payload: { x, y, z } }),
      reducer: (state, { payload }: PayloadAction<Point3D>) => {
        state.camera.towards = payload;
      },
    },
    setCameraProjectionVfov: (state, { payload }: PayloadAction<number>) => {
      if (payload > 0 && payload < 180) {
        state.camera.projection.vfov = payload;
      }
    },
    setCameraProjectionNear: (state, { payload }: PayloadAction<number>) => {
      state.camera.projection.near = payload;
      if (payload >= state.camera.projection.far) {
        state.camera.projection.far = payload + 1;
      }
    },
    setCameraProjectionFar: (state, { payload }: PayloadAction<number>) => {
      state.camera.projection.far = payload;
      if (payload <= state.camera.projection.near) {
        state.camera.projection.near = payload - 1;
      }
    },
    setObjectPosition: {
      prepare: (x: number, y: number, z: number) => ({ payload: { x, y, z } }),
      reducer: (state, { payload }: PayloadAction<Point3D>) => {
        state.object.position = payload;
      },
    },
    setObjectOrientationX: (state, { payload }: PayloadAction<number>) => {
      state.object.orientation.x = ((payload % 360) + 360) % 360;
    },
    setObjectOrientationY: (state, { payload }: PayloadAction<number>) => {
      state.object.orientation.y = ((payload % 360) + 360) % 360;
    },
    setObjectOrientationZ: (state, { payload }: PayloadAction<number>) => {
      state.object.orientation.z = ((payload % 360) + 360) % 360;
    },
    setObjectColor: (state, { payload }: PayloadAction<Color>) => {
      state.object.color = payload;
    },
    setLightSourcePosition: {
      prepare: (x: number, y: number, z: number) => ({ payload: { x, y, z } }),
      reducer: (state, { payload }: PayloadAction<Point3D>) => {
        state.lightSource.position = payload;
      },
    },
    setCameraTowardsFollowsCamera: (
      state,
      { payload }: PayloadAction<boolean>
    ) => {
      state.camera.followers.cameraTowards = payload;
    },
    setLightSourcePositionFollowsCamera: (
      state,
      { payload }: PayloadAction<boolean>
    ) => {
      state.camera.followers.lightSourcePosition = payload;
    },
    setCameraMovementMode: (
      state,
      { payload }: PayloadAction<MovementMode>
    ) => {
      state.camera.movement = payload;
    },
    setAll: (
      state,
      { payload }: PayloadAction<Omit<RendererOptions, 'resetTo'>>
    ) => {
      return { ...payload, resetTo: payload };
    },
    resetAll: ({ resetTo }) => {
      return { ...resetTo, resetTo };
    },
    deserialize: (state, { payload }: PayloadAction<URLSearchParams>) => {
      const cpx = payload.get('cpx');
      const cpy = payload.get('cpy');
      const cpz = payload.get('cpz');
      const ctx = payload.get('ctx');
      const cty = payload.get('cty');
      const ctz = payload.get('ctz');
      const vfov = payload.get('vfov');
      const near = payload.get('near');
      const far = payload.get('far');
      const ctfollow = payload.get('ctfollow');
      const lpfollow = payload.get('lpfollow');
      const movemode = payload.get('movemode');
      const opx = payload.get('opx');
      const opy = payload.get('opy');
      const opz = payload.get('opz');
      const oox = payload.get('oox');
      const ooy = payload.get('ooy');
      const ooz = payload.get('ooz');
      const oc = payload.get('oc');
      const lpx = payload.get('lpx');
      const lpy = payload.get('lpy');
      const lpz = payload.get('lpz');
      const partialState = {
        camera: {
          position: {
            x: cpx !== null ? parseFloat(cpx) : initialState.camera.position.x,
            y: cpy !== null ? parseFloat(cpy) : initialState.camera.position.y,
            z: cpz !== null ? parseFloat(cpz) : initialState.camera.position.z,
          },
          towards: {
            x: ctx !== null ? parseFloat(ctx) : initialState.camera.towards.x,
            y: cty !== null ? parseFloat(cty) : initialState.camera.towards.y,
            z: ctz !== null ? parseFloat(ctz) : initialState.camera.towards.z,
          },
          projection: {
            vfov:
              vfov !== null
                ? parseInt(vfov)
                : initialState.camera.projection.vfov,
            near:
              near !== null
                ? parseFloat(near)
                : initialState.camera.projection.near,
            far:
              far !== null
                ? parseFloat(far)
                : initialState.camera.projection.far,
          },
          followers: {
            cameraTowards:
              ctfollow !== null
                ? !!parseInt(ctfollow)
                : initialState.camera.followers.cameraTowards,
            lightSourcePosition:
              lpfollow !== null
                ? !!parseInt(lpfollow)
                : initialState.camera.followers.lightSourcePosition,
          },
          movement:
            movemode !== null
              ? parseInt(movemode)
              : initialState.camera.movement,
        },
        object: {
          position: {
            x: opx !== null ? parseFloat(opx) : initialState.object.position.x,
            y: opy !== null ? parseFloat(opy) : initialState.object.position.y,
            z: opz !== null ? parseFloat(opz) : initialState.object.position.z,
          },
          orientation: {
            x:
              oox !== null
                ? parseFloat(oox)
                : initialState.object.orientation.x,
            y:
              ooy !== null
                ? parseFloat(ooy)
                : initialState.object.orientation.y,
            z:
              ooz !== null
                ? parseFloat(ooz)
                : initialState.object.orientation.z,
          },
          color:
            oc !== null
              ? {
                  r: parseInt(oc.substring(0, 2), 16),
                  g: parseInt(oc.substring(2, 4), 16),
                  b: parseInt(oc.substring(4, 6), 16),
                }
              : initialState.object.color,
        },
        lightSource: {
          position: {
            x:
              lpx !== null
                ? parseFloat(lpx)
                : initialState.lightSource.position.x,
            y:
              lpy !== null
                ? parseFloat(lpy)
                : initialState.lightSource.position.y,
            z:
              lpz !== null
                ? parseFloat(lpz)
                : initialState.lightSource.position.z,
          },
        },
      };
      return { ...partialState, resetTo: partialState };
    },
  },
});

export const {
  moveCamera,
  zoomIn,
  zoomOut,
  setCameraPosition,
  setCameraTowards,
  setCameraProjectionVfov,
  setCameraProjectionNear,
  setCameraProjectionFar,
  setObjectPosition,
  setObjectOrientationX,
  setObjectOrientationY,
  setObjectOrientationZ,
  setObjectColor,
  setLightSourcePosition,
  setCameraTowardsFollowsCamera,
  setLightSourcePositionFollowsCamera,
  setCameraMovementMode,
  resetAll,
  setAll,
  deserialize,
} = rendererSlice.actions;

export const selectLightSourcePosition = (state: RootState) =>
  state.renderer.lightSource.position;
export const selectLightSourcePositionString = createSelector(
  selectLightSourcePosition,
  ({ x, y, z }) => `(${round(x, 3)}, ${round(y, 3)}, ${round(z, 3)})`
);
export const selectCameraPosition = (state: RootState) =>
  state.renderer.camera.position;
export const selectCameraPositionVector = createSelector(
  selectCameraPosition,
  ({ x, y, z }) => new Matrix([x, y, z])
);
export const selectCameraPositionString = createSelector(
  selectCameraPosition,
  ({ x, y, z }) => `(${round(x, 3)}, ${round(y, 3)}, ${round(z, 3)})`
);
export const selectCameraTowards = (state: RootState) =>
  state.renderer.camera.towards;
export const selectCameraTowardsVector = createSelector(
  selectCameraTowards,
  ({ x, y, z }) => new Matrix([x, y, z])
);
export const selectCameraTowardsString = createSelector(
  selectCameraTowards,
  ({ x, y, z }) => `(${round(x, 3)}, ${round(y, 3)}, ${round(z, 3)})`
);
export const selectCameraProjection = (state: RootState) =>
  state.renderer.camera.projection;
export const selectCameraProjectionVfov = (state: RootState) =>
  state.renderer.camera.projection.vfov;
export const selectCameraProjectionNear = (state: RootState) =>
  state.renderer.camera.projection.near;
export const selectCameraProjectionFar = (state: RootState) =>
  state.renderer.camera.projection.far;
export const selectObjectPosition = (state: RootState) =>
  state.renderer.object.position;
export const selectObjectPositionVector = createSelector(
  selectObjectPosition,
  ({ x, y, z }) => new Matrix([x, y, z])
);
export const selectObjectPositionString = createSelector(
  selectObjectPosition,
  ({ x, y, z }) => `(${round(x, 3)}, ${round(y, 3)}, ${round(z, 3)})`
);
export const selectObjectOrientation = (state: RootState) =>
  state.renderer.object.orientation;
export const selectObjectOrientationX = (state: RootState) =>
  state.renderer.object.orientation.x;
export const selectObjectOrientationY = (state: RootState) =>
  state.renderer.object.orientation.y;
export const selectObjectOrientationZ = (state: RootState) =>
  state.renderer.object.orientation.z;
export const selectObjectColor = (state: RootState) =>
  state.renderer.object.color;
export const selectObjectColorHexString = (state: RootState) =>
  '#' +
  Math.round(state.renderer.object.color.r).toString(16).padStart(2, '0') +
  Math.round(state.renderer.object.color.g).toString(16).padStart(2, '0') +
  Math.round(state.renderer.object.color.b).toString(16).padStart(2, '0');
export const selectCameraDirectionVector = createSelector(
  [selectCameraPositionVector, selectCameraTowardsVector],
  (cameraPositionVector, cameraTowardsVector) =>
    Matrix.subtract(cameraTowardsVector, cameraPositionVector)
);
export const selectObjectViewTransform = createSelector(
  [selectObjectPositionVector, selectCameraDirectionVector],
  (objectPositionVector, cameraDirectionVector) =>
    Transform3D.lookAt(
      objectPositionVector,
      Matrix.add(objectPositionVector, cameraDirectionVector)
    )
);
export const selectObjectViewInverseTransform = createSelector(
  [selectObjectViewTransform],
  (objectViewTransform) => Transform3D.inverseLookAt(objectViewTransform)
);
export const selectCameraTowardsFollowsCamera = (state: RootState) =>
  state.renderer.camera.followers.cameraTowards;
export const selectLightSourcePositionFollowsCamera = (state: RootState) =>
  state.renderer.camera.followers.lightSourcePosition;
export const selectCameraMovementMode = (state: RootState) =>
  state.renderer.camera.movement;

export const selectRendererSlice = (state: RootState) => state.renderer;
export const selectPersistentRendererSlice = createSelector(
  selectRendererSlice,
  ({ resetTo, ...renderer }) => renderer
);
export const serializeRendererSlice = (
  renderer: ReturnType<typeof selectPersistentRendererSlice>,
  params: URLSearchParams
) => {
  if (
    renderer.camera.position.x !== initialState.camera.position.x ||
    renderer.camera.position.y !== initialState.camera.position.y ||
    renderer.camera.position.z !== initialState.camera.position.z
  ) {
    params.set('cpx', `${round(renderer.camera.position.x, 3)}`);
    params.set('cpy', `${round(renderer.camera.position.y, 3)}`);
    params.set('cpz', `${round(renderer.camera.position.z, 3)}`);
  }
  if (
    renderer.camera.towards.x !== initialState.camera.towards.x ||
    renderer.camera.towards.y !== initialState.camera.towards.y ||
    renderer.camera.towards.z !== initialState.camera.towards.z
  ) {
    params.set('ctx', `${round(renderer.camera.towards.x, 3)}`);
    params.set('cty', `${round(renderer.camera.towards.y, 3)}`);
    params.set('ctz', `${round(renderer.camera.towards.z, 3)}`);
  }
  if (renderer.camera.projection.vfov !== initialState.camera.projection.vfov) {
    params.set('vfov', `${renderer.camera.projection.vfov}`);
  }
  if (renderer.camera.projection.near !== initialState.camera.projection.near) {
    params.set('near', `${renderer.camera.projection.near}`);
  }
  if (renderer.camera.projection.far !== initialState.camera.projection.far) {
    params.set('far', `${renderer.camera.projection.far}`);
  }
  if (
    renderer.camera.followers.cameraTowards !==
    initialState.camera.followers.cameraTowards
  ) {
    params.set('ctfollow', `${+renderer.camera.followers.cameraTowards}`);
  }
  if (
    renderer.camera.followers.lightSourcePosition !==
    initialState.camera.followers.lightSourcePosition
  ) {
    params.set('lpfollow', `${+renderer.camera.followers.lightSourcePosition}`);
  }
  if (renderer.camera.movement !== initialState.camera.movement) {
    params.set('movemode', `${renderer.camera.movement}`);
  }
  if (
    renderer.object.position.x !== initialState.object.position.x ||
    renderer.object.position.y !== initialState.object.position.y ||
    renderer.object.position.z !== initialState.object.position.z
  ) {
    params.set('opx', `${round(renderer.object.position.x, 3)}`);
    params.set('opy', `${round(renderer.object.position.y, 3)}`);
    params.set('opz', `${round(renderer.object.position.z, 3)}`);
  }
  if (
    renderer.object.orientation.x !== initialState.object.orientation.x ||
    renderer.object.orientation.y !== initialState.object.orientation.y ||
    renderer.object.orientation.z !== initialState.object.orientation.z
  ) {
    params.set('oox', `${round(renderer.object.orientation.x, 3)}`);
    params.set('ooy', `${round(renderer.object.orientation.y, 3)}`);
    params.set('ooz', `${round(renderer.object.orientation.z, 3)}`);
  }
  if (
    renderer.object.color.r !== initialState.object.color.r ||
    renderer.object.color.g !== initialState.object.color.g ||
    renderer.object.color.b !== initialState.object.color.b
  ) {
    params.set(
      'oc',
      Math.round(renderer.object.color.r).toString(16).padStart(2, '0') +
        Math.round(renderer.object.color.g).toString(16).padStart(2, '0') +
        Math.round(renderer.object.color.b).toString(16).padStart(2, '0')
    );
  }
  if (
    renderer.lightSource.position.x !== initialState.lightSource.position.x ||
    renderer.lightSource.position.y !== initialState.lightSource.position.y ||
    renderer.lightSource.position.z !== initialState.lightSource.position.z
  ) {
    params.set('lpx', `${round(renderer.lightSource.position.x, 3)}`);
    params.set('lpy', `${round(renderer.lightSource.position.y, 3)}`);
    params.set('lpz', `${round(renderer.lightSource.position.z, 3)}`);
  }
};
export const depersistRendererSlice = (
  renderer: ReturnType<typeof selectPersistentRendererSlice>,
  dispatch: typeof store.dispatch
) => dispatch(setAll(renderer));
export const deserializeRendererSlice = (
  params: URLSearchParams,
  dispatch: typeof store.dispatch
) => dispatch(deserialize(params));

export default rendererSlice.reducer;
