// https://github.com/purple-technology/react-camera-pro
import React, {
  useState,
  useEffect,
  useRef,
  useImperativeHandle,
  useCallback,
} from 'react';
import styled from '@emotion/styled';

type FacingMode = 'user' | 'environment';
type AspectRatio = 'cover' | number; // for example 16/9, 4/3, 1/1
type Stream = MediaStream | null;
type SetStream = React.Dispatch<React.SetStateAction<Stream>>;
type SetNumberOfCameras = React.Dispatch<React.SetStateAction<number>>;

interface ErrorMessages {
  noCameraAccessible?: string;
  permissionDenied?: string;
  switchCamera?: string;
  canvas?: string;
}

interface CameraProps {
  facingMode?: FacingMode;
  aspectRatio?: AspectRatio;
  numberOfCamerasCallback?(numberOfCameras: number): void;
  videoSourceDeviceId?: string | undefined;
  errorMessages?: ErrorMessages;
  videoReadyCallback?(): void;
  flashMode?: boolean;
  onError?: (error?: string) => void;
  isTouchSupported?: boolean;
  touchSupportedCallback?(touchSupported: boolean): void;
}

export type CameraType = React.ForwardRefExoticComponent<
  CameraProps & React.RefAttributes<unknown>
> & {
  takePhoto(): string;
  switchCamera(): FacingMode;
  getNumberOfCameras(): number;
  flashStatus(): boolean;
};

const Wrapper = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
`;

const Container = styled.div<{ aspectRatio: AspectRatio }>`
  width: 100%;
  ${({ aspectRatio }) =>
    aspectRatio === 'cover'
      ? `
    position: absolute;
    bottom: 0;
    top: 0;
    left: 0;
    right: 0;`
      : `
    position: relative;
    padding-bottom: ${100 / aspectRatio}%;`}
`;

const CameraVideo = styled.video<{ mirrored: boolean }>`
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
  transform: rotateY(${({ mirrored }) => (mirrored ? '180deg' : '0deg')});
`;

const Canvas = styled.canvas`
  display: none;
`;

export const Camera = React.forwardRef<unknown, CameraProps>(
  (
    {
      facingMode = 'user',
      aspectRatio = 'cover',
      videoSourceDeviceId = undefined,
      errorMessages = {
        noCameraAccessible:
          'No camera device accessible. Please connect your camera or try a different browser.',
        permissionDenied:
          'Permission denied. Please refresh and give camera permission.',
        switchCamera:
          'It is not possible to switch camera to different one because there is only one video device accessible.',
        canvas: 'Canvas is not supported.',
      },
      videoReadyCallback = () => null,
      touchSupportedCallback = () => null,
      flashMode = false,
      isTouchSupported = false,
      onError = () => null,
    },
    ref
  ) => {
    const player = useRef<HTMLVideoElement>(null);
    const canvas = useRef<HTMLCanvasElement>(null);
    const container = useRef<HTMLDivElement>(null);
    const [numberOfCameras, setNumberOfCameras] = useState<number>(0);
    const [stream, setStream] = useState<Stream>(null);
    const [currentFacingMode, setFacingMode] = useState<FacingMode>(facingMode);

    // const [torchsupported, setTorchSupported] = useState<boolean>(false);

    // useEffect(() => {
    //   touchSupportedCallback(torchsupported);
    // }, [torchsupported, touchSupportedCallback]);

    const toggleTorch = useCallback(
      (isOn: boolean): void => {
        const currentTrack =
          stream?.getVideoTracks()[0] || stream?.getTracks()[0];

        if (isTouchSupported) {
          currentTrack?.applyConstraints({
            advanced: [{ torch: isOn }],
          } as MediaTrackConstraintSet);
        } else {
          // console.log('Torch not supported');
        }
      },
      [isTouchSupported, stream]
    );

    useEffect(() => {
      toggleTorch(flashMode);
    }, [flashMode, toggleTorch]);

    useImperativeHandle(ref, () => ({
      takePhoto: () => {
        if (numberOfCameras < 1) {
          throw new Error(errorMessages.noCameraAccessible);
        }

        if (canvas?.current) {
          const playerWidth = player?.current?.videoWidth || 1280;
          const playerHeight = player?.current?.videoHeight || 720;
          const playerAR = playerWidth / playerHeight;

          const canvasWidth = container?.current?.offsetWidth || 1280;
          const canvasHeight = container?.current?.offsetHeight || 1280;
          const canvasAR = canvasWidth / canvasHeight;

          let sX, sY, sW, sH;

          if (playerAR > canvasAR) {
            sH = playerHeight;
            sW = playerHeight * canvasAR;
            sX = (playerWidth - sW) / 2;
            sY = 0;
          } else {
            sW = playerWidth;
            sH = playerWidth / canvasAR;
            sX = 0;
            sY = (playerHeight - sH) / 2;
          }

          canvas.current.width = sW;
          canvas.current.height = sH;

          const context = canvas.current.getContext('2d');
          if (context && player?.current) {
            context.drawImage(player.current, sX, sY, sW, sH, 0, 0, sW, sH);
          }

          const imgData = canvas.current.toDataURL('image/jpeg');
          return imgData;
        } else {
          onError(errorMessages.canvas);
          throw new Error(errorMessages.canvas);
        }
      },
      switchCamera: () => {
        if (numberOfCameras < 1) {
          onError(errorMessages.noCameraAccessible);
          throw new Error(errorMessages.noCameraAccessible);
        } else if (numberOfCameras < 2) {
          onError(
            'Error: Unable to switch camera. Only one device is accessible.'
          ); // console only
        }
        const newFacingMode =
          currentFacingMode === 'user' ? 'environment' : 'user';
        setFacingMode(newFacingMode);
        return newFacingMode;
      },
      getNumberOfCameras: () => {
        return numberOfCameras;
      },
      flashStatus: () => () => {
        return isTouchSupported;
      },
    }));

    useEffect(() => {
      initCameraStream(
        stream,
        setStream,
        currentFacingMode,
        videoSourceDeviceId,
        setNumberOfCameras,
        touchSupportedCallback,
        onError,
        errorMessages
      );
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [currentFacingMode, videoSourceDeviceId]);

    useEffect(() => {
      if (stream && player && player.current) {
        player.current.srcObject = stream;
      }
      return () => {
        if (stream) {
          stream.getTracks().forEach((track) => {
            track.stop();
          });
        }
      };
    }, [stream]);

    return (
      <Container ref={container} aspectRatio={aspectRatio}>
        <Wrapper>
          {/* {notSupported ? (
            <ErrorMsg>{errorMessages.noCameraAccessible}</ErrorMsg>
          ) : null}
          {permissionDenied ? (
            <ErrorMsg>{errorMessages.permissionDenied}</ErrorMsg>
          ) : null} */}
          <CameraVideo
            ref={player}
            id="video"
            muted={true}
            autoPlay={true}
            playsInline={true}
            controls={false}
            mirrored={currentFacingMode === 'user' ? true : false}
            onLoadedData={() => {
              videoReadyCallback();
            }}
          />
          <Canvas ref={canvas} />
        </Wrapper>
      </Container>
    );
  }
);

Camera.displayName = 'Camera';

const initCameraStream = (
  stream: Stream,
  setStream: SetStream,
  currentFacingMode: FacingMode,
  videoSourceDeviceId: string | undefined,
  setNumberOfCameras: SetNumberOfCameras,
  setTorchSupported?: (touchSupported: boolean) => void,
  onError?: (error: string) => void,
  errorMessages?: ErrorMessages
) => {
  // stop any active streams in the window
  if (stream) {
    stream.getTracks().forEach((track) => {
      track.stop();
    });
  }

  const constraints = {
    audio: false,
    video: {
      deviceId: videoSourceDeviceId
        ? { exact: videoSourceDeviceId }
        : undefined,
      facingMode: currentFacingMode,
      width: { ideal: 1920 },
      height: { ideal: 1920 },
    },
  };

  if (navigator?.mediaDevices?.getUserMedia) {
    navigator.mediaDevices
      .getUserMedia(constraints)
      .then(async (stream) => {
        const result = await checkTorchExists(navigator?.mediaDevices, stream);
        setTorchSupported?.(result);
        setStream(handleSuccess(stream, setNumberOfCameras));
      })
      .catch((err) => {
        handleError(err, onError, errorMessages);
      });
  } else {
    // const getWebcam =
    //   navigator.getUserMedia ||
    //   navigator.webkitGetUserMedia ||
    //   navigator.mozGetUserMedia ||
    //   navigator.msGetUserMedia;
    // if (getWebcam) {
    //   getWebcam(
    //     constraints,
    //     async (mediaStream: MediaStream) => {
    //       const result = await checkTorchExists(
    //         navigator?.mediaDevices,
    //         mediaStream
    //       );
    //       setTorchSupported(result);
    //       setStream(handleSuccess(mediaStream, setNumberOfCameras));
    //     },
    //     (error: Error) => {
    //       handleError(error, onError, errorMessages);
    //     }
    //   );
    // }
  }
};

const handleSuccess = (
  stream: MediaStream,
  setNumberOfCameras: SetNumberOfCameras
) => {
  navigator.mediaDevices
    .enumerateDevices()
    .then((r) =>
      setNumberOfCameras(r.filter((i) => i.kind === 'videoinput').length)
    );

  return stream;
};

const handleError = (
  error: Error,
  onError?: (error: string) => void,
  errorMessages?: ErrorMessages
) => {
  console.error(error.name);

  //https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia

  if (
    error.name === 'PermissionDeniedError' ||
    error.name === 'NotAllowedError'
  ) {
    // setPermissionDenied(true);
    onError?.(errorMessages?.permissionDenied || 'Permission denied');
  } else {
    // setNotSupported(true);
    onError?.(
      errorMessages?.noCameraAccessible || 'No camera device accessible'
    );
  }
};

async function checkTorchExists(device: any, stream: MediaStream) {
  const supportedConstraints = device?.getSupportedConstraints();
  let track = stream.getTracks()[0];
  try {
    if (
      supportedConstraints &&
      'torch' in supportedConstraints &&
      track &&
      track.applyConstraints
    ) {
      try {
        await track.applyConstraints({
          advanced: [{ torch: false }],
        } as MediaTrackConstraintSet);
        return true;
      } catch (error) {
        return false;
      }
    } else {
      return false;
    }
  } catch {
    return false;
  }
}
