import { useState, useCallback, useEffect } from 'react';
import { sortByKey } from '../lib/sortByKey';

export const DEVICE_STATE_LOADING = 'loading';
export const DEVICE_STATE_PENDING = 'pending';
export const DEVICE_STATE_ERROR = 'error';
export const DEVICE_STATE_GRANTED = 'granted';
export const DEVICE_STATE_NOT_FOUND = 'not-found';
export const DEVICE_STATE_NOT_SUPPORTED = 'not-supported';
export const DEVICE_STATE_BLOCKED = 'blocked';
export const DEVICE_STATE_IN_USE = 'in-use';
export const DEVICE_STATE_OFF = 'off';
export const DEVICE_STATE_PLAYABLE = 'playable';
export const DEVICE_STATE_SENDABLE = 'sendable';

export const useDevices = (callObject) => {
  const [deviceState, setDeviceState] = useState(DEVICE_STATE_LOADING);
  const [currentDevices, setCurrentDevices] = useState(null);

  const [cams, setCams] = useState([]);
  const [mics, setMics] = useState([]);
  const [speakers, setSpeakers] = useState([]);

  const [camError, setCamError] = useState(null);
  const [micError, setMicError] = useState(null);

  const updateDeviceState = useCallback(async () => {
    if (
      typeof navigator?.mediaDevices?.getUserMedia === 'undefined' ||
      typeof navigator?.mediaDevices?.enumerateDevices === 'undefined'
    ) {
      setDeviceState(DEVICE_STATE_NOT_SUPPORTED);
      return;
    }

    try {
      const { devices } = await callObject.enumerateDevices();

      const { camera, mic, speaker } = await callObject.getInputDevices();

      const [defaultCam, ...videoDevices] = devices.filter(
        (d) => d.kind === 'videoinput' && d.deviceId !== ''
      );
      setCams(
        [
          defaultCam,
          ...videoDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
        ].filter(Boolean)
      );
      const [defaultMic, ...micDevices] = devices.filter(
        (d) => d.kind === 'audioinput' && d.deviceId !== ''
      );
      setMics(
        [
          defaultMic,
          ...micDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
        ].filter(Boolean)
      );
      const [defaultSpeaker, ...speakerDevices] = devices.filter(
        (d) => d.kind === 'audiooutput' && d.deviceId !== ''
      );
      setSpeakers(
        [
          defaultSpeaker,
          ...speakerDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
        ].filter(Boolean)
      );

      setCurrentDevices({
        camera,
        mic,
        speaker,
      });

      console.log(`Current cam: ${camera.label}`);
      console.log(`Current mic: ${mic.label}`);
      console.log(`Current speakers: ${speaker.label}`);
    } catch (e) {
      setDeviceState(DEVICE_STATE_NOT_SUPPORTED);
    }
  }, [callObject]);

  const updateDeviceErrors = useCallback(() => {
    if (!callObject) return;
    const { tracks } = callObject.participants().local;

    if (tracks.video?.blocked?.byPermissions) {
      setCamError(DEVICE_STATE_BLOCKED);
    } else if (tracks.video?.blocked?.byDeviceMissing) {
      setCamError(DEVICE_STATE_NOT_FOUND);
    } else if (tracks.video?.blocked?.byDeviceInUse) {
      setCamError(DEVICE_STATE_IN_USE);
    }

    if (
      [
        DEVICE_STATE_LOADING,
        DEVICE_STATE_OFF,
        DEVICE_STATE_PLAYABLE,
        DEVICE_STATE_SENDABLE,
      ].includes(tracks.video.state)
    ) {
      setCamError(null);
    }

    if (tracks.audio?.blocked?.byPermissions) {
      setMicError(DEVICE_STATE_BLOCKED);
    } else if (tracks.audio?.blocked?.byDeviceMissing) {
      setMicError(DEVICE_STATE_NOT_FOUND);
    } else if (tracks.audio?.blocked?.byDeviceInUse) {
      setMicError(DEVICE_STATE_IN_USE);
    }

    if (
      [
        DEVICE_STATE_LOADING,
        DEVICE_STATE_OFF,
        DEVICE_STATE_PLAYABLE,
        DEVICE_STATE_SENDABLE,
      ].includes(tracks.audio.state)
    ) {
      setMicError(null);
    }
  }, [callObject]);

  const handleParticipantUpdated = useCallback(
    ({ participant }) => {
      if (!callObject || !participant.local) return;

      setDeviceState((prevState) => {
        if (prevState === DEVICE_STATE_NOT_SUPPORTED) return prevState;
        switch (participant?.tracks.video.state) {
          case DEVICE_STATE_BLOCKED:
            updateDeviceErrors();
            return DEVICE_STATE_ERROR;
          case DEVICE_STATE_OFF:
          case DEVICE_STATE_PLAYABLE:
            if (prevState === DEVICE_STATE_GRANTED) {
              return prevState;
            }
            updateDeviceState();
            return DEVICE_STATE_GRANTED;
          default:
            return prevState;
        }
      });
    },
    [callObject, updateDeviceState, updateDeviceErrors]
  );

  useEffect(() => {
    if (!callObject) return false;

    /**
      If the user is slow to allow access, we'll update the device state
      so our app can show a prompt requesting access
    */
    let pendingAccessTimeout;

    const handleJoiningMeeting = () => {
      pendingAccessTimeout = setTimeout(() => {
        setDeviceState(DEVICE_STATE_PENDING);
      }, 2000);
    };

    const handleJoinedMeeting = () => {
      clearTimeout(pendingAccessTimeout);
      // Note: setOutputDevice() is not honored before join() so we must enumerate again
      updateDeviceState();
    };

    callObject.on('joining-meeting', handleJoiningMeeting);
    callObject.on('joined-meeting', handleJoinedMeeting);
    callObject.on('participant-updated', handleParticipantUpdated);
    return () => {
      console.log('UNMOUNT');
      clearTimeout(pendingAccessTimeout);
      callObject.off('joining-meeting', handleJoiningMeeting);
      callObject.off('joined-meeting', handleJoinedMeeting);
      callObject.off('participant-updated', handleParticipantUpdated);
    };
  }, [callObject, handleParticipantUpdated, updateDeviceState]);

  const setCamDevice = useCallback(
    async (newCam, useLocalStorage = true) => {
      if (!callObject || newCam.deviceId === currentDevices?.cam?.deviceId) {
        return;
      }

      console.log(`🔛 Changing camera device to: ${newCam.label}`);

      if (useLocalStorage) {
        localStorage.setItem('defaultCamId', newCam.deviceId);
      }

      await callObject.setInputDevicesAsync({
        videoDeviceId: newCam.deviceId,
      });

      setCurrentDevices((prev) => ({ ...prev, camera: newCam }));
    },
    [callObject, currentDevices]
  );

  const setMicDevice = useCallback(
    async (newMic, useLocalStorage = true) => {
      if (!callObject || newMic.deviceId === currentDevices?.mic?.deviceId) {
        return;
      }

      console.log(`🔛 Changing mic device to: ${newMic.label}`);

      if (useLocalStorage) {
        localStorage.setItem('defaultMicId', newMic.deviceId);
      }

      await callObject.setInputDevicesAsync({
        audioDeviceId: newMic.deviceId,
      });

      setCurrentDevices((prev) => ({ ...prev, mic: newMic }));
    },
    [callObject, currentDevices]
  );

  const setSpeakersDevice = useCallback(
    async (newSpeakers, useLocalStorage = true) => {
      if (
        !callObject ||
        newSpeakers.deviceId === currentDevices?.speaker?.deviceId
      ) {
        return;
      }

      console.log(`Changing speakers device to: ${newSpeakers.label}`);

      if (useLocalStorage) {
        localStorage.setItem('defaultSpeakersId', newSpeakers.deviceId);
      }

      callObject.setOutputDevice({
        outputDeviceId: newSpeakers.deviceId,
      });

      setCurrentDevices((prev) => ({ ...prev, speaker: newSpeakers }));
    },
    [callObject, currentDevices]
  );

  useEffect(() => {
    if (!callObject) return false;

    console.log('💻 Device provider events bound');

    const handleCameraError = ({
      errorMsg: { errorMsg, audioOk, videoOk },
      error,
    }) => {
      switch (error?.type) {
        case 'cam-in-use':
          setDeviceState(DEVICE_STATE_ERROR);
          setCamError(DEVICE_STATE_IN_USE);
          break;
        case 'mic-in-use':
          setDeviceState(DEVICE_STATE_ERROR);
          setMicError(DEVICE_STATE_IN_USE);
          break;
        case 'cam-mic-in-use':
          setDeviceState(DEVICE_STATE_ERROR);
          setCamError(DEVICE_STATE_IN_USE);
          setMicError(DEVICE_STATE_IN_USE);
          break;
        default:
          switch (errorMsg) {
            case 'devices error':
              setDeviceState(DEVICE_STATE_ERROR);
              setCamError(videoOk ? null : DEVICE_STATE_NOT_FOUND);
              setMicError(audioOk ? null : DEVICE_STATE_NOT_FOUND);
              break;
            case 'not allowed':
              setDeviceState(DEVICE_STATE_ERROR);
              updateDeviceErrors();
              break;
            default:
              break;
          }
          break;
      }
    };

    const handleError = ({ errorMsg }) => {
      switch (errorMsg) {
        case 'not allowed':
          setDeviceState(DEVICE_STATE_ERROR);
          updateDeviceErrors();
          break;
        default:
          break;
      }
    };

    const handleStartedCamera = () => {
      updateDeviceErrors();
    };

    callObject.on('camera-error', handleCameraError);
    callObject.on('error', handleError);
    callObject.on('started-camera', handleStartedCamera);
    return () => {
      callObject.off('camera-error', handleCameraError);
      callObject.off('error', handleError);
      callObject.off('started-camera', handleStartedCamera);
    };
  }, [callObject, updateDeviceErrors]);

  return {
    cams,
    mics,
    speakers,
    camError,
    micError,
    currentDevices,
    deviceState,
    setCamDevice,
    setMicDevice,
    setSpeakersDevice,
  };
};

export default useDevices;
