package org.thoughtcrime.securesms.service.webrtc.state;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.annimon.stream.OptionalLong;

import org.signal.ringrtc.CallId;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.components.sensors.Orientation;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.WebRtcActionProcessor;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;

import java.util.Collection;
import java.util.Set;

/**
 * Builder that creates a new {@link WebRtcServiceState} from an existing one and allows
 * changes to all normally immutable data.
 */
public class WebRtcServiceStateBuilder {
  private WebRtcServiceState toBuild;

  public WebRtcServiceStateBuilder(@NonNull WebRtcServiceState webRtcServiceState) {
    toBuild = new WebRtcServiceState(webRtcServiceState);
  }

  public @NonNull WebRtcServiceState build() {
    return toBuild;
  }

  public @NonNull WebRtcServiceStateBuilder actionProcessor(@NonNull WebRtcActionProcessor actionHandler) {
    toBuild.actionProcessor = actionHandler;
    return this;
  }

  public @NonNull CallSetupStateBuilder changeCallSetupState(@NonNull CallId callId) {
    return new CallSetupStateBuilder(callId);
  }

  public @NonNull CallInfoStateBuilder changeCallInfoState() {
    return new CallInfoStateBuilder();
  }

  public @NonNull LocalDeviceStateBuilder changeLocalDeviceState() {
    return new LocalDeviceStateBuilder();
  }

  public @NonNull VideoStateBuilder changeVideoState() {
    return new VideoStateBuilder();
  }

  public @NonNull WebRtcServiceStateBuilder terminate(@NonNull CallId callId) {
    toBuild.localDeviceState = new LocalDeviceState();
    toBuild.videoState       = new VideoState();

    CallInfoState newCallInfoState = new CallInfoState();
    newCallInfoState.peerMap.putAll(toBuild.callInfoState.peerMap);
    toBuild.callInfoState = newCallInfoState;

    toBuild.callSetupStates.remove(callId);

    return this;
  }

  public class LocalDeviceStateBuilder {
    private LocalDeviceState toBuild;

    public LocalDeviceStateBuilder() {
      toBuild = WebRtcServiceStateBuilder.this.toBuild.localDeviceState.duplicate();
    }

    public @NonNull WebRtcServiceStateBuilder commit() {
      WebRtcServiceStateBuilder.this.toBuild.localDeviceState = toBuild;
      return WebRtcServiceStateBuilder.this;
    }

    public @NonNull WebRtcServiceState build() {
      commit();
      return WebRtcServiceStateBuilder.this.build();
    }

    public @NonNull LocalDeviceStateBuilder cameraState(@NonNull CameraState cameraState) {
      toBuild.setCameraState(cameraState);
      return this;
    }

    public @NonNull LocalDeviceStateBuilder isMicrophoneEnabled(boolean enabled) {
      toBuild.setMicrophoneEnabled(enabled);
      return this;
    }

    public @NonNull LocalDeviceStateBuilder setOrientation(@NonNull Orientation orientation) {
      toBuild.setOrientation(orientation);
      return this;
    }

    public @NonNull LocalDeviceStateBuilder setLandscapeEnabled(boolean isLandscapeEnabled) {
      toBuild.setLandscapeEnabled(isLandscapeEnabled);
      return this;
    }

    public @NonNull LocalDeviceStateBuilder setDeviceOrientation(@NonNull Orientation deviceOrientation) {
      toBuild.setDeviceOrientation(deviceOrientation);
      return this;
    }

    public @NonNull LocalDeviceStateBuilder setActiveDevice(@NonNull SignalAudioManager.AudioDevice audioDevice) {
      toBuild.setActiveDevice(audioDevice);
      return this;
    }

    public @NonNull LocalDeviceStateBuilder setAvailableDevices(@NonNull Set<SignalAudioManager.AudioDevice> availableDevices) {
      toBuild.setAvailableDevices(availableDevices);
      return this;
    }
  }

  public class CallSetupStateBuilder {
    private CallSetupState toBuild;
    private CallId         callId;

    public CallSetupStateBuilder(@NonNull CallId callId) {
      this.toBuild = WebRtcServiceStateBuilder.this.toBuild.getCallSetupState(callId).duplicate();
      this.callId  = callId;
    }

    public @NonNull WebRtcServiceStateBuilder commit() {
      WebRtcServiceStateBuilder.this.toBuild.callSetupStates.put(callId, toBuild);
      return WebRtcServiceStateBuilder.this;
    }

    public @NonNull WebRtcServiceState build() {
      commit();
      return WebRtcServiceStateBuilder.this.build();
    }

    public @NonNull CallSetupStateBuilder enableVideoOnCreate(boolean enableVideoOnCreate) {
      toBuild.setEnableVideoOnCreate(enableVideoOnCreate);
      return this;
    }

    public @NonNull CallSetupStateBuilder isRemoteVideoOffer(boolean isRemoteVideoOffer) {
      toBuild.setRemoteVideoOffer(isRemoteVideoOffer);
      return this;
    }

    public @NonNull CallSetupStateBuilder acceptWithVideo(boolean acceptWithVideo) {
      toBuild.setAcceptWithVideo(acceptWithVideo);
      return this;
    }

    public @NonNull CallSetupStateBuilder sentJoinedMessage(boolean sentJoinedMessage) {
      toBuild.setSentJoinedMessage(sentJoinedMessage);
      return this;
    }

    public @NonNull CallSetupStateBuilder setRingGroup(boolean ringGroup) {
      toBuild.setRingGroup(ringGroup);
      return this;
    }

    public @NonNull CallSetupStateBuilder ringId(long ringId) {
      toBuild.setRingId(ringId);
      return this;
    }

    public @NonNull CallSetupStateBuilder ringerRecipient(@NonNull Recipient ringerRecipient) {
      toBuild.setRingerRecipient(ringerRecipient);
      return this;
    }
  }

  public class VideoStateBuilder {
    private VideoState toBuild;

    public VideoStateBuilder() {
      toBuild = new VideoState(WebRtcServiceStateBuilder.this.toBuild.videoState);
    }

    public @NonNull WebRtcServiceStateBuilder commit() {
      WebRtcServiceStateBuilder.this.toBuild.videoState = toBuild;
      return WebRtcServiceStateBuilder.this;
    }

    public @NonNull WebRtcServiceState build() {
      commit();
      return WebRtcServiceStateBuilder.this.build();
    }

    public @NonNull VideoStateBuilder eglBase(@Nullable EglBaseWrapper eglBase) {
      toBuild.eglBase = eglBase;
      return this;
    }

    public @NonNull VideoStateBuilder localSink(@Nullable BroadcastVideoSink localSink) {
      toBuild.localSink = localSink;
      return this;
    }

    public @NonNull VideoStateBuilder camera(@Nullable Camera camera) {
      toBuild.camera = camera;
      return this;
    }
  }

  public class CallInfoStateBuilder {
    private CallInfoState toBuild;

    public CallInfoStateBuilder() {
      toBuild = new CallInfoState(WebRtcServiceStateBuilder.this.toBuild.callInfoState);
    }

    public @NonNull WebRtcServiceStateBuilder commit() {
      WebRtcServiceStateBuilder.this.toBuild.callInfoState = toBuild;
      return WebRtcServiceStateBuilder.this;
    }

    public @NonNull WebRtcServiceState build() {
      commit();
      return WebRtcServiceStateBuilder.this.build();
    }

    public @NonNull CallInfoStateBuilder callState(@NonNull WebRtcViewModel.State callState) {
      toBuild.callState = callState;
      return this;
    }

    public @NonNull CallInfoStateBuilder callRecipient(@NonNull Recipient callRecipient) {
      toBuild.callRecipient = callRecipient;
      return this;
    }

    public @NonNull CallInfoStateBuilder callConnectedTime(long callConnectedTime) {
      toBuild.callConnectedTime = callConnectedTime;
      return this;
    }

    public @NonNull CallInfoStateBuilder putParticipant(@NonNull CallParticipantId callParticipantId, @NonNull CallParticipant callParticipant) {
      toBuild.remoteParticipants.put(callParticipantId, callParticipant);
      return this;
    }

    public @NonNull CallInfoStateBuilder putParticipant(@NonNull Recipient recipient, @NonNull CallParticipant callParticipant) {
      toBuild.remoteParticipants.put(new CallParticipantId(recipient), callParticipant);
      return this;
    }

    public @NonNull CallInfoStateBuilder clearParticipantMap() {
      toBuild.remoteParticipants.clear();
      return this;
    }

    public @NonNull CallInfoStateBuilder putRemotePeer(@NonNull RemotePeer remotePeer) {
      toBuild.peerMap.put(remotePeer.hashCode(), remotePeer);
      return this;
    }

    public @NonNull CallInfoStateBuilder clearPeerMap() {
      toBuild.peerMap.clear();
      return this;
    }

    public @NonNull CallInfoStateBuilder removeRemotePeer(@NonNull RemotePeer remotePeer) {
      toBuild.peerMap.remove(remotePeer.hashCode());
      return this;
    }

    public @NonNull CallInfoStateBuilder activePeer(@Nullable RemotePeer activePeer) {
      toBuild.activePeer = activePeer;
      return this;
    }

    public @NonNull CallInfoStateBuilder groupCall(@Nullable GroupCall groupCall) {
      toBuild.groupCall = groupCall;
      return this;
    }

    public @NonNull CallInfoStateBuilder groupCallState(@Nullable WebRtcViewModel.GroupCallState groupState) {
      toBuild.groupState = groupState;
      return this;
    }

    public @NonNull CallInfoStateBuilder addIdentityChangedRecipients(@NonNull Collection<RecipientId> id) {
      toBuild.identityChangedRecipients.addAll(id);
      return this;
    }

    public @NonNull CallInfoStateBuilder removeIdentityChangedRecipients(@NonNull Collection<RecipientId> ids) {
      toBuild.identityChangedRecipients.removeAll(ids);
      return this;
    }

    public @NonNull CallInfoStateBuilder remoteDevicesCount(long remoteDevicesCount) {
      toBuild.remoteDevicesCount = OptionalLong.of(remoteDevicesCount);
      return this;
    }

    public @NonNull CallInfoStateBuilder participantLimit(@Nullable Long participantLimit) {
      toBuild.participantLimit = participantLimit;
      return this;
    }
  }
}
