import React, { useCallback, useRef, ChangeEvent, useContext } from 'react';
import { useSelector, useDispatch } from '../../app/store';
import {
  selectCameraPositionString,
  setCameraPosition,
  selectCameraTowardsString,
  setCameraTowards,
  selectCameraProjectionVfov,
  setCameraProjectionVfov,
  selectCameraProjectionNear,
  setCameraProjectionNear,
  selectCameraProjectionFar,
  setCameraProjectionFar,
  selectObjectPositionString,
  setObjectPosition,
  selectObjectOrientationX,
  setObjectOrientationX,
  selectObjectOrientationY,
  setObjectOrientationY,
  selectObjectOrientationZ,
  setObjectOrientationZ,
  selectObjectColorHexString,
  setObjectColor,
  selectLightSourcePositionString,
  setLightSourcePosition,
  selectCameraTowardsFollowsCamera,
  setCameraTowardsFollowsCamera,
  selectLightSourcePositionFollowsCamera,
  setLightSourcePositionFollowsCamera,
  resetAll,
  MovementMode,
  selectCameraMovementMode,
  setCameraMovementMode,
} from '../canvas/rendererSlice';
import styles from './InfoBox.module.css';
import useTextBox from '../../hooks/useTextBox';
import { HistoryContext } from '../history/HistoryContext';

function parseTriple(string: string) {
  let trimmed = string.trim();
  if (trimmed.startsWith('(') && trimmed.endsWith(')')) {
    trimmed = trimmed.slice(1, -1).trim();
  }
  const split = trimmed.split(',');
  if (split.length !== 3) {
    return;
  }
  const parsed = split.map((value) => parseFloat(value));
  if (parsed.some((value) => isNaN(value))) {
    return;
  }
  return parsed;
}

function parseColor(color: string) {
  const r = parseInt(color.substring(1, 3), 16);
  const g = parseInt(color.substring(3, 5), 16);
  const b = parseInt(color.substring(5, 7), 16);
  if (isNaN(r) || isNaN(g) || isNaN(b)) {
    return;
  }
  return { r, g, b };
}

export function InfoBox() {
  const dispatch = useDispatch();
  const { navigate } = useContext(HistoryContext);
  const movementMode = useSelector(selectCameraMovementMode);
  const onMovementModeChange = useCallback(
    (event: ChangeEvent<HTMLSelectElement>) => {
      dispatch(setCameraMovementMode(parseInt(event.target.value)));
    },
    [dispatch]
  );
  const cameraPositionString = useSelector(selectCameraPositionString);
  const onCameraPositionChange = useCallback(
    (value: string) => {
      const parsed = parseTriple(value);
      if (parsed) {
        const [x, y, z] = parsed;
        dispatch(setCameraPosition(x, y, z));
      }
    },
    [dispatch]
  );
  const cameraPositionInputRef = useRef<HTMLInputElement | null>(null);
  const cameraPositionTextBox = useTextBox(
    cameraPositionString,
    onCameraPositionChange,
    cameraPositionInputRef
  );
  const cameraTowardsString = useSelector(selectCameraTowardsString);
  const onCameraTowardsChange = useCallback(
    (value: string) => {
      const parsed = parseTriple(value);
      if (parsed) {
        const [x, y, z] = parsed;
        dispatch(setCameraTowards(x, y, z));
      }
    },
    [dispatch]
  );
  const cameraTowardsInputRef = useRef<HTMLInputElement | null>(null);
  const cameraTowardsTextBox = useTextBox(
    cameraTowardsString,
    onCameraTowardsChange,
    cameraTowardsInputRef
  );
  const cameraProjectionVfov = useSelector(selectCameraProjectionVfov);
  const onCameraProjectionVfovChange = useCallback(
    (event: ChangeEvent<HTMLInputElement>) => {
      const parsed = parseInt(event.target.value);
      if (!isNaN(parsed)) {
        dispatch(setCameraProjectionVfov(parsed));
      }
    },
    [dispatch]
  );
  const cameraProjectionNear = useSelector(selectCameraProjectionNear);
  const cameraProjectionNearString = `${cameraProjectionNear}`;
  const onCameraProjectionNearChange = useCallback(
    (value: string) => dispatch(setCameraProjectionNear(parseFloat(value))),
    [dispatch]
  );
  const cameraProjectionNearInputRef = useRef<HTMLInputElement | null>(null);
  const cameraProjectionNearTextBox = useTextBox(
    cameraProjectionNearString,
    onCameraProjectionNearChange,
    cameraProjectionNearInputRef
  );
  const cameraProjectionFar = useSelector(selectCameraProjectionFar);
  const cameraProjectionFarString = `${cameraProjectionFar}`;
  const onCameraProjectionFarChange = useCallback(
    (value: string) => dispatch(setCameraProjectionFar(parseFloat(value))),
    [dispatch]
  );
  const cameraProjectionFarInputRef = useRef<HTMLInputElement | null>(null);
  const cameraProjectionFarTextBox = useTextBox(
    cameraProjectionFarString,
    onCameraProjectionFarChange,
    cameraProjectionFarInputRef
  );
  const objectPositionString = useSelector(selectObjectPositionString);
  const onObjectPositionChange = useCallback(
    (value: string) => {
      const parsed = parseTriple(value);
      if (parsed) {
        const [x, y, z] = parsed;
        dispatch(setObjectPosition(x, y, z));
      }
    },
    [dispatch]
  );
  const objectPositionInputRef = useRef<HTMLInputElement | null>(null);
  const objectPositionTextBox = useTextBox(
    objectPositionString,
    onObjectPositionChange,
    objectPositionInputRef
  );
  const objectOrientationX = useSelector(selectObjectOrientationX);
  const objectOrientationXString = `${objectOrientationX}`;
  const onObjectOrientationXChange = useCallback(
    (value: string) => dispatch(setObjectOrientationX(parseFloat(value))),
    [dispatch]
  );
  const objectOrientationXInputRef = useRef<HTMLInputElement | null>(null);
  const objectOrientationXTextBox = useTextBox(
    objectOrientationXString,
    onObjectOrientationXChange,
    objectOrientationXInputRef,
    true
  );
  const objectOrientationY = useSelector(selectObjectOrientationY);
  const objectOrientationYString = `${objectOrientationY}`;
  const onObjectOrientationYChange = useCallback(
    (value: string) => dispatch(setObjectOrientationY(parseFloat(value))),
    [dispatch]
  );
  const objectOrientationYInputRef = useRef<HTMLInputElement | null>(null);
  const objectOrientationYTextBox = useTextBox(
    objectOrientationYString,
    onObjectOrientationYChange,
    objectOrientationYInputRef,
    true
  );
  const objectOrientationZ = useSelector(selectObjectOrientationZ);
  const objectOrientationZString = `${objectOrientationZ}`;
  const onObjectOrientationZChange = useCallback(
    (value: string) => dispatch(setObjectOrientationZ(parseFloat(value))),
    [dispatch]
  );
  const objectOrientationZInputRef = useRef<HTMLInputElement | null>(null);
  const objectOrientationZTextBox = useTextBox(
    objectOrientationZString,
    onObjectOrientationZChange,
    objectOrientationZInputRef,
    true
  );
  const objectColorHexString = useSelector(selectObjectColorHexString);
  const onObjectColorChange = useCallback(
    (value: string) => {
      const parsed = parseColor(value);
      if (parsed) {
        dispatch(setObjectColor(parsed));
      }
    },
    [dispatch]
  );
  const objectColorInputRef = useRef<HTMLInputElement | null>(null);
  const objectColorTextBox = useTextBox(
    objectColorHexString,
    onObjectColorChange,
    objectColorInputRef
  );
  const lightSourcePositionString = useSelector(
    selectLightSourcePositionString
  );
  const onLightSourcePositionChange = useCallback(
    (value: string) => {
      const parsed = parseTriple(value);
      if (parsed) {
        const [x, y, z] = parsed;
        dispatch(setLightSourcePosition(x, y, z));
      }
    },
    [dispatch]
  );
  const lightSourcePositionInputRef = useRef<HTMLInputElement | null>(null);
  const lightSourcePositionTextBox = useTextBox(
    lightSourcePositionString,
    onLightSourcePositionChange,
    lightSourcePositionInputRef
  );
  const cameraTowardsFollowsCamera = useSelector(
    selectCameraTowardsFollowsCamera
  );
  const onCameraTowardsFollowsCameraChange = useCallback(
    (event: ChangeEvent<HTMLInputElement>) => {
      dispatch(setCameraTowardsFollowsCamera(event.target.checked));
    },
    [dispatch]
  );
  const lightSourcePositionFollowsCamera = useSelector(
    selectLightSourcePositionFollowsCamera
  );
  const onLightSourcePositionFollowsCameraChange = useCallback(
    (event: ChangeEvent<HTMLInputElement>) => {
      dispatch(setLightSourcePositionFollowsCamera(event.target.checked));
    },
    [dispatch]
  );
  const onResetAllClick = useCallback(() => dispatch(resetAll()), [dispatch]);
  const onBackToHomepageClick = useCallback(() => navigate('/'), [navigate]);

  return (
    <div className={styles.infobox}>
      <h3>Controls</h3>
      <div className={styles.attributes}>
        <div className={styles.attribute}>
          <label>
            <div className={styles.label}>Mode</div>
            <div className={styles.input}>
              <select value={movementMode} onChange={onMovementModeChange}>
                <option value={MovementMode.ORBIT}>Orbit</option>
                <option value={MovementMode.TRANSLATE}>Translate</option>
                <option value={MovementMode.ROTATE}>Rotate</option>
              </select>
            </div>
          </label>
        </div>
        <div className={styles.attribute}>
          <div className={styles.help}>
            {movementMode === MovementMode.ORBIT && (
              <>
                Drag to move the camera around the look-at point.
                <br />
                <kbd>Shift</kbd> + drag to move closer to or further from the
                look-at point.
                <br />
                Scroll to zoom in or out.
              </>
            )}
            {movementMode === MovementMode.TRANSLATE && (
              <>
                Drag to translate the camera in the x-y plane.
                <br />
                <kbd>Shift</kbd> + drag to translate in the x-z plane.
                <br />
                Scroll to zoom in or out.
              </>
            )}
            {movementMode === MovementMode.ROTATE && (
              <>
                Drag to rotate the camera in-place.
                <br />
                Scroll to zoom in or out.
              </>
            )}
          </div>
        </div>
      </div>
      <hr />
      <h3>Camera</h3>
      <div className={styles.attributes}>
        <div className={styles.attribute}>
          <label>
            <div className={styles.label}>Position</div>
            <div className={styles.input}>
              <input
                type="text"
                value={cameraPositionTextBox.value}
                onChange={cameraPositionTextBox.onChange}
                ref={cameraPositionInputRef}
                required
              />
            </div>
          </label>
        </div>
        <div className={styles.attribute}>
          <label>
            <div className={styles.label}>Pointed towards</div>
            <div className={styles.input}>
              <input
                type="text"
                value={cameraTowardsTextBox.value}
                onChange={cameraTowardsTextBox.onChange}
                ref={cameraTowardsInputRef}
                required
              />
            </div>
          </label>
          <label className={styles.extra}>
            <input
              type="checkbox"
              checked={cameraTowardsFollowsCamera}
              onChange={onCameraTowardsFollowsCameraChange}
            />
            <span>Follow camera movement</span>
          </label>
        </div>
      </div>
      <hr />
      <h3>Projection</h3>
      <div className={styles.attributes}>
        <div className={styles.attribute}>
          <label>
            <div className={styles.label}>
              <abbr title="Vertical field of view">VFOV</abbr>
            </div>
            <div className={styles.input}>
              <input
                type="range"
                min={1}
                max={179}
                value={cameraProjectionVfov}
                onChange={onCameraProjectionVfovChange}
              />
              <span>{cameraProjectionVfov}&deg;</span>
            </div>
          </label>
        </div>
        <div className={styles.attribute}>
          <div className={styles.multiattribute}>
            <div>
              <label>
                <div className={styles.label}>Near</div>
                <div className={styles.input}>
                  <input
                    type="number"
                    min={0.001}
                    step="any"
                    value={cameraProjectionNearTextBox.value}
                    onChange={cameraProjectionNearTextBox.onChange}
                    ref={cameraProjectionNearInputRef}
                    required
                  />
                </div>
              </label>
            </div>
            <div>
              <label>
                <div className={styles.label}>Far</div>
                <div className={styles.input}>
                  <input
                    type="number"
                    min={1.001}
                    step="any"
                    value={cameraProjectionFarTextBox.value}
                    onChange={cameraProjectionFarTextBox.onChange}
                    ref={cameraProjectionFarInputRef}
                    required
                  />
                </div>
              </label>
            </div>
          </div>
        </div>
      </div>
      <hr />
      <h3>Object</h3>
      <div className={styles.attributes}>
        <div className={styles.attribute}>
          <label>
            <div className={styles.label}>Position</div>
            <div className={styles.input}>
              <input
                type="text"
                value={objectPositionTextBox.value}
                onChange={objectPositionTextBox.onChange}
                ref={objectPositionInputRef}
                required
              />
            </div>
          </label>
        </div>
        <div className={styles.attribute}>
          <div className={styles.label}>Rotation about</div>
          <div className={styles.multiattribute}>
            <label>
              <div className={styles.label}>x-axis</div>
              <div className={styles.input}>
                <input
                  type="number"
                  step="any"
                  value={objectOrientationXTextBox.value}
                  onChange={objectOrientationXTextBox.onChange}
                  ref={objectOrientationXInputRef}
                  required
                />
                &deg;
              </div>
            </label>
            <label>
              <div className={styles.label}>y-axis</div>
              <div className={styles.input}>
                <input
                  type="number"
                  step="any"
                  value={objectOrientationYTextBox.value}
                  onChange={objectOrientationYTextBox.onChange}
                  ref={objectOrientationYInputRef}
                  required
                />
                &deg;
              </div>
            </label>
            <label>
              <div className={styles.label}>z-axis</div>
              <div className={styles.input}>
                <input
                  type="number"
                  step="any"
                  value={objectOrientationZTextBox.value}
                  onChange={objectOrientationZTextBox.onChange}
                  ref={objectOrientationZInputRef}
                  required
                />
                &deg;
              </div>
            </label>
          </div>
        </div>
        <div className={styles.attribute}>
          <label>
            <div className={styles.label}>Color</div>
            <div className={styles.input}>
              <input
                type="color"
                value={objectColorTextBox.value}
                onChange={objectColorTextBox.onChange}
                ref={objectColorInputRef}
                required
              />
            </div>
          </label>
        </div>
      </div>
      <hr />
      <h3>Light Source</h3>
      <div className={styles.attributes}>
        <div className={styles.attribute}>
          <label>
            <div className={styles.label}>Position</div>
            <div className={styles.input}>
              <input
                type="text"
                value={lightSourcePositionTextBox.value}
                onChange={lightSourcePositionTextBox.onChange}
                ref={lightSourcePositionInputRef}
                required
              />
            </div>
          </label>
          <label className={styles.extra}>
            <input
              type="checkbox"
              checked={lightSourcePositionFollowsCamera}
              onChange={onLightSourcePositionFollowsCameraChange}
            />
            <span>Follow camera movement</span>
          </label>
        </div>
      </div>
      <hr />
      <div className={styles.buttons}>
        <button onClick={onResetAllClick}>Reset All</button>
        <button onClick={onBackToHomepageClick}>Back to Home</button>
      </div>
    </div>
  );
}
