content: package im.actor.core.modules.calls.peers;
  
  import java.util.HashMap;
  import java.util.List;
  
  import im.actor.core.api.ApiICEServer;
  import im.actor.core.modules.ModuleContext;
  import im.actor.core.modules.calls.peers.messages.RTCAdvertised;
  import im.actor.core.modules.calls.peers.messages.RTCAnswer;
  import im.actor.core.modules.calls.peers.messages.RTCCandidate;
  import im.actor.core.modules.calls.peers.messages.RTCCloseSession;
  import im.actor.core.modules.calls.peers.messages.RTCDispose;
  import im.actor.core.modules.calls.peers.messages.RTCMasterAdvertised;
  import im.actor.core.modules.calls.peers.messages.RTCMediaStateUpdated;
  import im.actor.core.modules.calls.peers.messages.RTCNeedOffer;
  import im.actor.core.modules.calls.peers.messages.RTCOffer;
  import im.actor.core.modules.calls.peers.messages.RTCStart;
  import im.actor.core.modules.ModuleActor;
  import im.actor.runtime.Log;
  import im.actor.runtime.WebRTC;
  import im.actor.runtime.actors.messages.PoisonPill;
  import im.actor.runtime.function.CountedReference;
  import im.actor.runtime.webrtc.WebRTCMediaStream;
  import im.actor.runtime.webrtc.WebRTCMediaTrack;
  
  /**
   * Manages Connections to all Nodes
   */
  public class PeerCallActor extends ModuleActor {
  
      private static final String TAG = "PeerCallActor";
  
      // Parent Actor for handling events
      private final PeerCallCallback callback;
  
      // Peer Settings
      private final PeerSettings selfSettings;
  
      // WebRTC objects
      private List<ApiICEServer> iceServers;
      private HashMap<Long, PeerNodeInt> refs = new HashMap<>();
  
      private CountedReference<WebRTCMediaStream> currentMediaStream;
      private boolean isCurrentStreamAudioRequired;
      private boolean isCurrentStreamVideoRequired;
      private boolean isAudioRequired;
      private boolean isVideoRequired;
  
      // State objects
      private boolean isOwnStarted = false;
      private boolean isBuildingStream = false;
      private boolean isAudioEnabled = true;
      private boolean isVideoEnabled = false;
  
      public PeerCallActor(PeerCallCallback callback, PeerSettings selfSettings, ModuleContext context) {
          super(context);
          this.callback = callback;
          this.selfSettings = selfSettings;
          this.isAudioRequired = true;
          this.isVideoRequired = false;
      }
  
      //
      // Media Settings
      //
  
      public void onAudioEnabled(boolean isAudioEnabled) {
          if (this.isAudioEnabled != isAudioEnabled) {
              this.isAudioEnabled = isAudioEnabled;
              if (!isAudioRequired) {
                  isAudioRequired = true;
              }
              if (currentMediaStream != null) {
                  for (WebRTCMediaTrack audio : currentMediaStream.get().getAudioTracks()) {
                      audio.setEnabled(isAudioEnabled);
                      if (isAudioEnabled) {
                          callback.onOwnTrackAdded(audio);
                      } else {
                          callback.onOwnTrackRemoved(audio);
                      }
                  }
              }
              requestStreamIfNeeded();
  
              for (Long deviceId : refs.keySet()) {
                  callback.onMediaStreamsChanged(deviceId, isAudioEnabled, isVideoEnabled);
              }
          }
      }
  
      public void onVideoEnabled(boolean isVideoEnabled) {
          if (this.isVideoEnabled != isVideoEnabled) {
              this.isVideoEnabled = isVideoEnabled;
              if (!isVideoRequired) {
                  isVideoRequired = true;
              }
              if (currentMediaStream != null) {
                  for (WebRTCMediaTrack video : currentMediaStream.get().getVideoTracks()) {
                      video.setEnabled(isVideoEnabled);
                      if (isVideoEnabled) {
                          callback.onOwnTrackAdded(video);
                      } else {
                          callback.onOwnTrackRemoved(video);
                      }
                  }
              }
              requestStreamIfNeeded();
  
              for (Long deviceId : refs.keySet()) {
                  callback.onMediaStreamsChanged(deviceId, isAudioEnabled, isVideoEnabled);
              }
          }
      }
  
      public void onOwnStarted() {
          if (isOwnStarted) {
              return;
          }
          isOwnStarted = true;
  
          requestStreamIfNeeded();
      }
  
      private void requestStreamIfNeeded() {
          if (!isBuildingStream && isOwnStarted
                  && ((currentMediaStream == null)
                  || (isCurrentStreamAudioRequired != isAudioRequired)
                  || (isCurrentStreamVideoRequired != isVideoRequired))) {
  
              isBuildingStream = true;
              isCurrentStreamAudioRequired = isAudioRequired;
              isCurrentStreamVideoRequired = isVideoRequired;
              WebRTC.getUserMedia(isAudioRequired, isVideoRequired).then(mediaStream -> {
                  isBuildingStream = false;
  
                  // Checking if required types are not changed
                  if (isCurrentStreamAudioRequired != isAudioRequired || isCurrentStreamVideoRequired != isVideoRequired) {
                      // Closing unwanted media stream
                      mediaStream.close();
                      requestStreamIfNeeded();
                      return;
                  }
  
                  // Update Stream Locally
                  CountedReference<WebRTCMediaStream> newStream = new VerboseReference(mediaStream);
                  CountedReference<WebRTCMediaStream> oldStream = currentMediaStream;
                  currentMediaStream = newStream;
  
                  // Releasing old stream
                  if (oldStream != null) {
                      if (isAudioEnabled) {
                          for (WebRTCMediaTrack track : oldStream.get().getAudioTracks()) {
                              callback.onOwnTrackRemoved(track);
                          }
                      }
                      if (isVideoEnabled) {
                          for (WebRTCMediaTrack track : oldStream.get().getVideoTracks()) {
                              callback.onOwnTrackRemoved(track);
                          }
                      }
                      oldStream.release();
                  }
  
                  // Connecting to a new stream
                  for (WebRTCMediaTrack audio : mediaStream.getAudioTracks()) {
                      audio.setEnabled(isAudioEnabled);
                      if (isAudioEnabled) {
                          callback.onOwnTrackAdded(audio);
                      }
                  }
                  for (WebRTCMediaTrack video : mediaStream.getVideoTracks()) {
                      video.setEnabled(isVideoEnabled);
                      if (isVideoEnabled) {
                          callback.onOwnTrackAdded(video);
                      }
                  }
  
                  // Update references in all active nodes
                  for (PeerNodeInt node : refs.values()) {
                      node.replaceOwnStream(newStream);
                  }
              }).failure(e -> {
                  Log.d(TAG, "Unable to load stream");
                  self().send(PoisonPill.INSTANCE);
              });
          }
      }
  
      public void onMasterAdvertised(List<ApiICEServer> iceServers) {
          if (this.iceServers == null) {
              this.iceServers = iceServers;
              for (PeerNodeInt node : refs.values()) {
                  node.onAdvertisedMaster(iceServers);
              }
          }
      }
  
      //
      // Peer Collection
      //
  
      public PeerNodeInt getPeer(long deviceId) {
          if (!refs.containsKey(deviceId)) {
              return createNewPeer(deviceId);
          }
          return refs.get(deviceId);
      }
  
      public PeerNodeInt createNewPeer(final long deviceId) {
          // Creating Peer Node
          PeerNodeInt peerNodeInt = new PeerNodeInt(deviceId, new NodeCallback(), selfSettings, self(), context());
          // Setting Own Stream if available
          if (currentMediaStream != null) {
              peerNodeInt.replaceOwnStream(currentMediaStream);
          }
          // Set advertise info if available
          if (this.iceServers != null) {
              peerNodeInt.onAdvertisedMaster(iceServers);
          }
  
          // Notify new node about current streams configuration
          callback.onMediaStreamsChanged(deviceId, isAudioEnabled, isVideoEnabled);
  
          refs.put(deviceId, peerNodeInt);
          return peerNodeInt;
      }
  
      public void disposePeer(long deviceId) {
          PeerNodeInt peerNodeInt = refs.remove(deviceId);
          if (peerNodeInt != null) {
              peerNodeInt.kill();
          }
      }
  
  
      @Override
      public void postStop() {
          super.postStop();
  
          for (PeerNodeInt d : refs.values()) {
              d.kill();
          }
          refs.clear();
  
          if (currentMediaStream != null) {
              if (isAudioEnabled) {
                  for (WebRTCMediaTrack track : currentMediaStream.get().getAudioTracks()) {
                      callback.onOwnTrackRemoved(track);
                  }
              }
              if (isVideoEnabled) {
                  for (WebRTCMediaTrack track : currentMediaStream.get().getVideoTracks()) {
                      callback.onOwnTrackRemoved(track);
                  }
              }
              currentMediaStream.release();
              currentMediaStream = null;
          }
      }
  
      @Override
      public void onReceive(Object message) {
          if (message instanceof RTCStart) {
              RTCStart start = (RTCStart) message;
              getPeer(start.getDeviceId()).startConnection();
          } else if (message instanceof RTCDispose) {
              RTCDispose dispose = (RTCDispose) message;
              disposePeer(dispose.getDeviceId());
          } else if (message instanceof RTCOffer) {
              RTCOffer offer = (RTCOffer) message;
              getPeer(offer.getDeviceId()).onOffer(offer.getSessionId(), offer.getSdp());
          } else if (message instanceof RTCAnswer) {
              RTCAnswer answer = (RTCAnswer) message;
              getPeer(answer.getDeviceId()).onAnswer(answer.getSessionId(), answer.getSdp());
          } else if (message instanceof RTCCandidate) {
              RTCCandidate candidate = (RTCCandidate) message;
              getPeer(candidate.getDeviceId()).onCandidate(candidate.getSessionId(), candidate.getMdpIndex(),
                      candidate.getId(), candidate.getSdp());
          } else if (message instanceof RTCNeedOffer) {
              RTCNeedOffer needOffer = (RTCNeedOffer) message;
              getPeer(needOffer.getDeviceId()).onOfferNeeded(needOffer.getSessionId());
          } else if (message instanceof RTCAdvertised) {
              RTCAdvertised advertised = (RTCAdvertised) message;
              getPeer(advertised.getDeviceId()).onAdvertised(advertised.getSettings());
          } else if (message instanceof RTCCloseSession) {
              RTCCloseSession closeSession = (RTCCloseSession) message;
              getPeer(closeSession.getDeviceId()).closeSession(closeSession.getSessionId());
          } else if (message instanceof RTCMediaStateUpdated) {
              RTCMediaStateUpdated mediaStateUpdated = (RTCMediaStateUpdated) message;
              getPeer(mediaStateUpdated.getDeviceId()).onMediaStateChanged(mediaStateUpdated.isAudioEnabled(),
                      mediaStateUpdated.isVideoEnabled());
          } else if (message instanceof RTCMasterAdvertised) {
              RTCMasterAdvertised masterAdvertised = (RTCMasterAdvertised) message;
              onMasterAdvertised(masterAdvertised.getIceServers());
          } else if (message instanceof AudioEnabled) {
              AudioEnabled muteChanged = (AudioEnabled) message;
              onAudioEnabled(muteChanged.isEnabled());
          } else if (message instanceof VideoEnabled) {
              VideoEnabled videoEnabled = (VideoEnabled) message;
              onVideoEnabled(videoEnabled.isEnabled());
          } else if (message instanceof OwnStarted) {
              onOwnStarted();
          } else {
              super.onReceive(message);
          }
      }
  
      public static class AudioEnabled {
          private boolean isEnabled;
  
          public AudioEnabled(boolean isEnabled) {
              this.isEnabled = isEnabled;
          }
  
          public boolean isEnabled() {
              return isEnabled;
          }
      }
  
      public static class VideoEnabled {
  
          private boolean enabled;
  
          public VideoEnabled(boolean enabled) {
              this.enabled = enabled;
          }
  
          public boolean isEnabled() {
              return enabled;
          }
      }
  
      public static class OwnStarted {
  
      }
  
      private class NodeCallback implements PeerNodeCallback {
  
          @Override
          public void onOffer(long deviceId, long sessionId, String sdp) {
              callback.onOffer(deviceId, sessionId, sdp);
          }
  
          @Override
          public void onAnswer(long deviceId, long sessionId, String sdp) {
              callback.onAnswer(deviceId, sessionId, sdp);
          }
  
          @Override
          public void onNegotiationSuccessful(long deviceId, long sessionId) {
              callback.onNegotiationSuccessful(deviceId, sessionId);
          }
  
          @Override
          public void onNegotiationNeeded(long deviceId, long sessionId) {
              callback.onNegotiationNeeded(deviceId, sessionId);
          }
  
          @Override
          public void onCandidate(long deviceId, long sessionId, int mdpIndex, String id, String sdp) {
              callback.onCandidate(deviceId, sessionId, mdpIndex, id, sdp);
          }
  
          @Override
          public void onPeerStateChanged(long deviceId, PeerState state) {
              callback.onPeerStateChanged(deviceId, state);
          }
  
          @Override
          public void onTrackAdded(long deviceId, WebRTCMediaTrack track) {
              callback.onTrackAdded(deviceId, track);
          }
  
          @Override
          public void onTrackRemoved(long deviceId, WebRTCMediaTrack track) {
              callback.onTrackRemoved(deviceId, track);
          }
      }
  
      private static int referenceId = 0;
  
      private static class VerboseReference extends CountedReference<WebRTCMediaStream> {
  
          private int id = referenceId++;
  
          public VerboseReference(WebRTCMediaStream value) {
              super(value);
          }
  
          @Override
          public synchronized void acquire(int counter) {
              Log.d("CountedReference(" + id + ")", "acquire(" + counter + ")");
          }
  
          @Override
          public synchronized void release(int counter) {
              Log.d("CountedReference(" + id + ")", "release(" + counter + ")");
          }
      }
  }