import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'
import {
  ConnectionQuality,
  Participant,
  SessionState,
  StreamKind,
  StreamSource,
} from '@zocks-communications/client-sdk-js'
import { RootState } from '../../app/store'
import { getSessionByRoomId, getTrackById } from '../utils'
import {
  AddRemoteParticipantAction,
  ConnectionQualityAction,
  LocalTrackPublishedAction,
  LocalTrackUnpublishedAction,
  RemoveRemoteParticipantAction,
  TrackMuteAction,
  TrackPublishedAction,
  TrackSubscribedAction,
  TrackUnpublishedAction,
  TrackUnsubscribedAction,
} from './actionModels'
import { ParticipantModel, ParticipantTracksView, SessionModel, TrackModel, TrackView } from './models'

const createNewParticipant = (id: string | null = null, name: string | null = null): ParticipantModel => {
  return {
    id,
    name,
    tracks: {
      videoTracks: [],
      audioTracks: [],
      screenShareTrack: null,
    },
    connectionQuality: ConnectionQuality.EXCELLENT,
  }
}

const initialState: SessionModel = {
  roomId: null,
  roomName: null,
  sessionState: SessionState.DISCONNECTED,
  isConnecting: false,
  localParticipant: createNewParticipant(),
  remoteParticipants: {},
  speakers: {
    activeSpeakers: [],
    lastSpeakers: [],
  },
}

export const sessionSlice = createSlice({
  name: 'session',
  initialState,
  reducers: {
    setSessionState: (state, action: PayloadAction<SessionState>) => {
      state.sessionState = action.payload
    },
    setRoomId: (state, action: PayloadAction<string>) => {
      state.roomId = action.payload
    },
    setRoomName: (state, action: PayloadAction<string>) => {
      state.roomName = action.payload
    },
    setConnecting: (state, action: PayloadAction<boolean>) => {
      state.isConnecting = action.payload
    },
    clearRoomId: (state) => {
      state.roomId = null
    },
    clearRoomName: (state) => {
      state.roomName = null
    },
    setLocalParticipantId: (state, action: PayloadAction<string>) => {
      state.localParticipant.id = action.payload
    },
    setLocalParticipantName: (state, action: PayloadAction<string | undefined>) => {
      state.localParticipant.name = action.payload || null
    },
    clearParticipants: (state) => {
      state.localParticipant = createNewParticipant()
      state.remoteParticipants = {}
      state.speakers.lastSpeakers = []
    },
    participantLocalTrackPublished: (state, action: PayloadAction<LocalTrackPublishedAction>) => {
      const { trackId, trackKind, trackSource, trackIsMuted } = action.payload
      addTrackForParticipant(state.localParticipant, trackKind, trackSource, trackId, trackIsMuted, true)
    },
    participantLocalTrackUnPublished: (state, action: PayloadAction<LocalTrackUnpublishedAction>) => {
      const { trackId, trackKind, trackSource } = action.payload
      removeTrackFromParticipant(state.localParticipant, trackKind, trackSource, trackId)
    },
    addRemoteParticipant: (state, action: PayloadAction<AddRemoteParticipantAction>) => {
      const { participantId, participantName } = action.payload
      state.remoteParticipants[participantId] = createNewParticipant(participantId, participantName)
    },
    removeRemoteParticipant: (state, action: PayloadAction<RemoveRemoteParticipantAction>) => {
      const { participantId } = action.payload
      delete state.remoteParticipants[participantId]
    },
    participantTrackSubscribed: (state, action: PayloadAction<TrackSubscribedAction>) => {
      const { participantId, trackId, trackIsMuted } = action.payload
      const remoteParticipant = state.remoteParticipants[participantId]
      if (remoteParticipant) {
        subscribeToParticipantTrack(remoteParticipant, trackId)
        changeParticipantTrackMuteStatus(trackIsMuted, remoteParticipant, trackId)
      }
    },
    participantTrackUnsubscribed: (state, action: PayloadAction<TrackUnsubscribedAction>) => {
      const { participantId, trackId } = action.payload
      const remoteParticipant = state.remoteParticipants[participantId]
      if (remoteParticipant) {
        unsubscribeFromParticipantTrack(remoteParticipant, trackId)
      }
    },
    participantTrackPublished: (state, action: PayloadAction<TrackPublishedAction>) => {
      const { participantId, trackId, trackKind, trackSource, trackIsMuted, trackIsSubscribed } = action.payload
      const remoteParticipant = state.remoteParticipants[participantId]
      if (remoteParticipant) {
        removeTrackFromParticipant(remoteParticipant, trackKind, trackSource, trackId)
        addTrackForParticipant(remoteParticipant, trackKind, trackSource, trackId, trackIsMuted, trackIsSubscribed)
      }
    },
    participantTrackUnPublished: (state, action: PayloadAction<TrackUnpublishedAction>) => {
      const { participantId, trackId, trackKind, trackSource } = action.payload
      const remoteParticipant = state.remoteParticipants[participantId]
      if (remoteParticipant) {
        removeTrackFromParticipant(remoteParticipant, trackKind, trackSource, trackId)
      }
    },
    participantTrackMuted: (state, action: PayloadAction<TrackMuteAction>) => {
      const { participantId, trackId } = action.payload
      const participant = getParticipantById(state, participantId)
      if (participant) {
        muteParticipantTrack(participant, trackId)
      }
    },
    participantTrackUnMuted: (state, action: PayloadAction<TrackMuteAction>) => {
      const { participantId, trackId } = action.payload
      const participant = getParticipantById(state, participantId)
      if (participant) {
        unMuteParticipantTrack(participant, trackId)
      }
    },
    connectionQualityChanged: (state, action: PayloadAction<ConnectionQualityAction>) => {
      const { participantId, connectionQuality } = action.payload
      const participant = getParticipantById(state, participantId)
      if (participant) {
        participant.connectionQuality = connectionQuality
      }
    },
    activeSpeakersChanged: (state, action: PayloadAction<string[]>) => {
      const newActiveSpeakers = action.payload
      const { speakers } = state
      speakers.activeSpeakers = newActiveSpeakers
      if (newActiveSpeakers.length) {
        const newLastSpeakers = [...new Set([...newActiveSpeakers, ...speakers.lastSpeakers])]
        if (newLastSpeakers.toString() !== speakers.lastSpeakers.toString()) {
          state.speakers.lastSpeakers = [...new Set([...newActiveSpeakers, ...speakers.lastSpeakers])]
        }
      }
    },
  },
})

export const {
  setSessionState,
  setRoomId,
  setRoomName,
  setConnecting,
  clearRoomId,
  clearRoomName,
  setLocalParticipantId,
  setLocalParticipantName,
  participantLocalTrackPublished,
  participantLocalTrackUnPublished,
  participantTrackPublished,
  participantTrackUnPublished,
  participantTrackSubscribed,
  participantTrackUnsubscribed,
  addRemoteParticipant,
  removeRemoteParticipant,
  clearParticipants,
  participantTrackMuted,
  participantTrackUnMuted,
  connectionQualityChanged,
  activeSpeakersChanged,
} = sessionSlice.actions

export const selectRoomId = (state: RootState): string | null => state.session.roomId

export const selectRoomName = (state: RootState): string | null => state.session.roomName

export const selectSessionState = (state: RootState): SessionState => state.session.sessionState

export const selectIsConnecting = (state: RootState): boolean => state.session.isConnecting

export const selectLocalParticipant = (state: RootState): ParticipantModel => state.session.localParticipant

export const selectActiveSpeakers = (state: RootState): string[] => state.session.speakers.activeSpeakers

export const selectLastSpeakers = (state: RootState): string[] => state.session.speakers.lastSpeakers

export const selectRemoteParticipants = createSelector(
  [(state) => state.session.remoteParticipants],
  (remoteParticipants): ParticipantModel[] => {
    return Object.values(remoteParticipants)
  }
)
export const selectLocalParticipantIsConnected = createSelector(
  [selectSessionState, selectLocalParticipant],
  (sessionState, localParticipant): boolean => {
    return sessionState === SessionState.CONNECTED && !!localParticipant.id
  }
)

export const selectIsScreenSharing = createSelector(
  [selectLocalParticipant, selectRemoteParticipants],
  (localParticipant, remoteParticipants): boolean => {
    const localParticipantScreenShare = !!localParticipant.tracks.screenShareTrack
    const remoteParticipantScreenShare = remoteParticipants.some(
      (remoteParticipant) => !!remoteParticipant.tracks.screenShareTrack
    )
    return localParticipantScreenShare || remoteParticipantScreenShare
  }
)

export const selectScreenSharingParticipant = createSelector(
  [selectLocalParticipant, (state) => state.session.remoteParticipants],
  (localParticipant, remoteParticipants: { [key: string]: ParticipantModel }): ParticipantModel | null => {
    if (localParticipant.tracks.screenShareTrack) {
      return localParticipant
    }
    return Object.values(remoteParticipants).find((participant) => !!participant.tracks.screenShareTrack) || null
  }
)

export const selectUnmutedRemoteParticipantAudioTracks = createSelector(
  [selectRoomId, selectSessionState, (state) => state.session.remoteParticipants],
  (roomId, sessionState, remoteParticipants: { [key: string]: ParticipantModel }): TrackView[] => {
    const zocksSession = getSessionByRoomId(roomId)
    if (sessionState !== SessionState.CONNECTED || !zocksSession) {
      return []
    }

    const speakingParticipantAudioTracks = []
    for (const [participantId, participant] of Object.entries(remoteParticipants)) {
      const {
        tracks: { audioTracks },
      } = participant
      if (audioTracks.length && audioTracks[0].isSubscribed && !audioTracks[0].isMuted) {
        const zocksParticipant = zocksSession?.participants.get(participantId)
        const zocksTrack = convertToTrackView(audioTracks[0], zocksParticipant)
        if (zocksTrack) {
          speakingParticipantAudioTracks.push(zocksTrack)
        }
      }
    }
    return speakingParticipantAudioTracks
  }
)

export const selectTracksForParticipant = createSelector(
  [selectRoomId, selectSessionState, (state, participant) => participant],
  (roomId, sessionState, participant): ParticipantTracksView => {
    const {
      id: participantId,
      tracks: { audioTracks, videoTracks, screenShareTrack },
    } = participant
    const zocksSession = getSessionByRoomId(roomId)
    if (sessionState !== SessionState.CONNECTED || !zocksSession || !participantId) {
      return {
        videoTracks: [],
        audioTracks: [],
        primaryVideoTrack: null,
        primaryAudioTrack: null,
        screenShareTrack: null,
      }
    }
    const zocksParticipant =
      participantId === zocksSession.localParticipant.id
        ? zocksSession.localParticipant
        : zocksSession.participants.get(participantId)
    const videoTrackViews = convertToTrackViews(videoTracks, zocksParticipant)
    const primaryVideoTrack = videoTrackViews.find((t) => t.isSubscribed && !t.isMuted) || videoTrackViews[0] || null
    const audioTrackViews = convertToTrackViews(audioTracks, zocksParticipant)
    const primaryAudioTrack = audioTrackViews.find((t) => t.isSubscribed && !t.isMuted) || audioTrackViews[0] || null
    return {
      videoTracks: videoTrackViews,
      audioTracks: audioTrackViews,
      primaryVideoTrack,
      primaryAudioTrack,
      screenShareTrack: (screenShareTrack && convertToTrackView(screenShareTrack, zocksParticipant)) || null,
    }
  }
)

const getParticipantById = (session: SessionModel, participantId: string): ParticipantModel | undefined => {
  const { localParticipant, remoteParticipants } = session
  return participantId === localParticipant.id ? localParticipant : remoteParticipants[participantId]
}

const addTrackForParticipant = (
  participant: ParticipantModel,
  trackKind: StreamKind,
  trackSource: StreamSource,
  trackId: string,
  isMuted: boolean,
  isSubscribed: boolean
) => {
  const { tracks } = participant
  if (trackSource === StreamSource.SCREEN_SHARE) {
    tracks.screenShareTrack = { id: trackId, isMuted, isSubscribed }
  } else if (trackKind === StreamKind.AUDIO) {
    tracks.audioTracks.push({ id: trackId, isMuted, isSubscribed })
  } else if (trackKind === StreamKind.VIDEO) {
    tracks.videoTracks.push({ id: trackId, isMuted, isSubscribed })
  }
}

const removeTrackFromParticipant = (
  participant: ParticipantModel,
  trackKind: StreamKind,
  trackSource: StreamSource,
  trackId: string
) => {
  const { tracks } = participant
  if (trackSource === StreamSource.SCREEN_SHARE) {
    tracks.screenShareTrack = null
  } else if (trackKind === StreamKind.AUDIO) {
    tracks.audioTracks = tracks.audioTracks.filter((track) => track.id !== trackId)
  } else if (trackKind === StreamKind.VIDEO) {
    tracks.videoTracks = tracks.videoTracks.filter((track) => track.id !== trackId)
  }
}

const muteParticipantTrack = (participant: ParticipantModel, trackId: string) => {
  changeParticipantTrackMuteStatus(true, participant, trackId)
}

const unMuteParticipantTrack = (participant: ParticipantModel, trackId: string) => {
  changeParticipantTrackMuteStatus(false, participant, trackId)
}

const changeParticipantTrackMuteStatus = (isMuted: boolean, participant: ParticipantModel, trackId: string) => {
  const { audioTracks, videoTracks } = participant.tracks
  const track = audioTracks.find((track) => track.id === trackId) || videoTracks.find((track) => track.id === trackId)
  if (track) {
    track.isMuted = isMuted
  }
}

const subscribeToParticipantTrack = (participant: ParticipantModel, trackId: string) => {
  changeParticipantTrackSubscribeStatus(true, participant, trackId)
}

const unsubscribeFromParticipantTrack = (participant: ParticipantModel, trackId: string) => {
  changeParticipantTrackSubscribeStatus(false, participant, trackId)
}

const changeParticipantTrackSubscribeStatus = (
  isSubscribed: boolean,
  participant: ParticipantModel,
  trackId: string
) => {
  const { audioTracks, videoTracks, screenShareTrack } = participant.tracks
  const track = audioTracks.find((track) => track.id === trackId) || videoTracks.find((track) => track.id === trackId)
  if (track) {
    track.isSubscribed = isSubscribed
  } else if (screenShareTrack && screenShareTrack.id === trackId) {
    screenShareTrack.isSubscribed = isSubscribed
  }
}

const convertToTrackViews = (tracks: TrackModel[], zocksParticipant?: Participant): TrackView[] => {
  return tracks.reduce((result: TrackView[], track: TrackModel) => {
    const trackView = convertToTrackView(track, zocksParticipant)
    if (trackView) {
      result.push(trackView)
    }
    return result
  }, [])
}

const convertToTrackView = (track: TrackModel, zocksParticipant?: Participant): TrackView | null => {
  const zocksTrack = getTrackById(zocksParticipant, track.id)
  return zocksTrack ? { track: zocksTrack, isMuted: track.isMuted, isSubscribed: track.isSubscribed } : null
}

export default sessionSlice.reducer
