content: package im.actor.core.modules.calls.peers;
  
  import java.util.ArrayList;
  import java.util.HashSet;
  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.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.actors.ask.AskMessage;
  import im.actor.runtime.actors.messages.Void;
  import im.actor.runtime.function.CountedReference;
  import im.actor.runtime.promise.Promise;
  import im.actor.runtime.webrtc.WebRTCMediaStream;
  import im.actor.runtime.webrtc.WebRTCMediaTrack;
  
  /**
   * Proxy Actor for simplifying state of PeerConnection by careful peer connection initialization
   * and handling case when we want to establish connection before call answering
   */
  public class PeerNodeActor extends ModuleActor implements PeerConnectionCallback {
  
      //
      // Node Configuration
      //
  
      /**
       * Current Node's DeviceId
       */
      private final long deviceId;
      /**
       * Callback for a Node events
       */
      private final PeerNodeCallback callback;
  
  
      //
      // Connection Configuration
      //
      private final PeerSettings ownSettings;
      private PeerSettings theirSettings;
      private List<ApiICEServer> iceServers;
      private CountedReference<WebRTCMediaStream> ownMediaStream;
  
  
      //
      // Signaling State
      //
  
      /**
       * Current Session Id
       */
      private long currentSession = 0;
      /**
       * All closed sessions. Used to filter out old signaling messages
       */
      private final HashSet<Long> closedSessions = new HashSet<>();
      /**
       * All pending sessions
       */
      private final ArrayList<PendingSession> pendingSessions = new ArrayList<>();
  
  
      //
      // Current peer connection
      //
      private int CHILD_NEXT_ID = 0;
      private PeerConnectionInt peerConnection;
      private WebRTCMediaStream theirStream;
  
      //
      // Node State Values
      //
  
      /**
       * Mean that if it can produce media tracks
       */
      private boolean isEnabled = false;
      /**
       * State if node is connected to other peer
       */
      private boolean isConnected = false;
      /**
       * State if node is connected, enabled and notified about all streams
       */
      private boolean isStarted = false;
      /**
       * External node state value
       */
      private PeerState state = PeerState.PENDING;
      /**
       * Is Node's audio enabled
       */
      private boolean isAudioEnabled = true;
      /**
       * Is Node's video enabled
       */
      private boolean isVideoEnabled = true;
  
  
      public PeerNodeActor(long deviceId,
                           PeerSettings ownSettings,
                           PeerNodeCallback callback,
                           ModuleContext context) {
          super(context);
          this.deviceId = deviceId;
          this.ownSettings = ownSettings;
          this.callback = callback;
      }
  
  
      //
      // Starting up peer nodes
      //
      // Stages:
      // 0. Notify about new peer node
      // 1. Waiting for master advertise
      // 2. Waiting for advertise of a node
      // 3. If both peers supports pre connection, create new connection
      // 4. Enabling Own and Their peers
      // 5. Setting own media stream
      // 6. Creation of peer connection
      //
  
      @Override
      public void preStart() {
          callback.onPeerStateChanged(deviceId, state);
      }
  
      public void onMasterAdvertised(List<ApiICEServer> iceServers) {
          if (this.iceServers == null) {
              this.iceServers = iceServers;
              reconfigurePeerConnectionIfNeeded();
          }
      }
  
      public void onAdvertised(PeerSettings settings) {
          if (this.theirSettings == null) {
              this.theirSettings = settings;
              reconfigurePeerConnectionIfNeeded();
          }
      }
  
      public void onEnabled() {
          if (!isEnabled) {
              isEnabled = true;
  
              reconfigurePeerConnectionIfNeeded();
          }
      }
  
      public Promise<Void> replaceOwnStream(CountedReference<WebRTCMediaStream> mediaStream) {
          if (this.ownMediaStream == null) {
              this.ownMediaStream = mediaStream;
              reconfigurePeerConnectionIfNeeded();
          } else {
              this.ownMediaStream.release();
              this.ownMediaStream = mediaStream;
              if (peerConnection != null) {
                  return peerConnection.replaceStream(mediaStream);
              }
          }
  
          return Promise.success(null);
      }
  
      private void reconfigurePeerConnectionIfNeeded() {
          makePeerConnectionIfNeeded();
          startIfNeeded();
      }
  
      private void makePeerConnectionIfNeeded() {
          if (peerConnection != null || theirSettings == null || ownMediaStream == null || this.iceServers == null) {
              return;
          }
  
          if (isEnabled || (theirSettings.isPreConnectionEnabled() && ownSettings.isPreConnectionEnabled())) {
              state = PeerState.CONNECTING;
              callback.onPeerStateChanged(deviceId, state);
              peerConnection = new PeerConnectionInt(
                      iceServers, ownSettings, theirSettings,
                      ownMediaStream, this, context(), self(), "connection/" + (CHILD_NEXT_ID++));
              unstashAll();
          }
      }
  
      private void startIfNeeded() {
          if (isEnabled && isConnected && !isStarted) {
              isStarted = true;
  
              state = PeerState.ACTIVE;
              callback.onPeerStateChanged(deviceId, state);
  
              if (theirStream != null) {
                  for (WebRTCMediaTrack track : theirStream.getAudioTracks()) {
                      track.setEnabled(isAudioEnabled);
                      if (isAudioEnabled) {
                          callback.onTrackAdded(deviceId, track);
                      }
                  }
                  for (WebRTCMediaTrack track : theirStream.getVideoTracks()) {
                      track.setEnabled(isVideoEnabled);
                      if (isVideoEnabled) {
                          callback.onTrackAdded(deviceId, track);
                      }
                  }
              }
          }
      }
  
  
      //
      // Peer callbacks
      //
  
      @Override
      public void onOffer(long sessionId, String sdp) {
          callback.onOffer(deviceId, sessionId, sdp);
      }
  
      @Override
      public void onAnswer(long sessionId, String sdp) {
          callback.onAnswer(deviceId, sessionId, sdp);
      }
  
      @Override
      public void onCandidate(long sessionId, int mdpIndex, String id, String sdp) {
          callback.onCandidate(deviceId, sessionId, mdpIndex, id, sdp);
      }
  
      @Override
      public void onNegotiationSuccessful(long sessionId) {
          callback.onNegotiationSuccessful(deviceId, sessionId);
      }
  
      @Override
      public void onNegotiationNeeded(long sessionId) {
          callback.onNegotiationNeeded(deviceId, sessionId);
      }
  
      @Override
      public void onStreamAdded(WebRTCMediaStream stream) {
          WebRTCMediaStream oldStream = theirStream;
          theirStream = stream;
  
          //
          // Enable Tracks if needed
          //
          if (isStarted) {
              for (WebRTCMediaTrack track : stream.getAudioTracks()) {
                  track.setEnabled(isAudioEnabled);
                  if (isAudioEnabled) {
                      callback.onTrackAdded(deviceId, track);
                  }
              }
              for (WebRTCMediaTrack track : stream.getVideoTracks()) {
                  track.setEnabled(isVideoEnabled);
                  if (isVideoEnabled) {
                      callback.onTrackAdded(deviceId, track);
                  }
              }
  
              if (oldStream != null) {
                  for (WebRTCMediaTrack track : oldStream.getVideoTracks()) {
                      callback.onTrackRemoved(deviceId, track);
                  }
                  for (WebRTCMediaTrack track : oldStream.getAudioTracks()) {
                      callback.onTrackRemoved(deviceId, track);
                  }
              }
          }
  
          if (!isConnected) {
              isConnected = true;
              if (!isEnabled) {
                  state = PeerState.CONNECTED;
                  callback.onPeerStateChanged(deviceId, state);
              } else {
                  // This case is handled in startIfNeeded();
              }
          }
  
          startIfNeeded();
      }
  
      @Override
  
      public void onStreamRemoved(WebRTCMediaStream stream) {
          if (isAudioEnabled || isVideoEnabled) {
              return;
          }
          //
          // Remove Tracks if needed
          //
          if (isStarted && theirStream != null) {
              for (WebRTCMediaTrack track : stream.getAudioTracks()) {
                  callback.onTrackRemoved(deviceId, track);
              }
              for (WebRTCMediaTrack track : stream.getVideoTracks()) {
                  callback.onTrackRemoved(deviceId, track);
              }
          }
      }
  
      public void onStreamStateChanged(boolean isAudioEnabled, boolean isVideoEnabled) {
          if (this.isAudioEnabled != isAudioEnabled) {
              this.isAudioEnabled = isAudioEnabled;
              if (isStarted) {
                  for (WebRTCMediaTrack track : theirStream.getAudioTracks()) {
                      track.setEnabled(isAudioEnabled);
                      if (isAudioEnabled) {
                          callback.onTrackAdded(deviceId, track);
                      } else {
                          callback.onTrackRemoved(deviceId, track);
                      }
                  }
              }
          }
          if (this.isVideoEnabled != isVideoEnabled) {
              this.isVideoEnabled = isVideoEnabled;
              if (isStarted) {
                  for (WebRTCMediaTrack track : theirStream.getVideoTracks()) {
                      track.setEnabled(isVideoEnabled);
                      if (isVideoEnabled) {
                          callback.onTrackAdded(deviceId, track);
                      } else {
                          callback.onTrackRemoved(deviceId, track);
                      }
                  }
              }
          }
      }
  
      public void onCloseSession(long sessionId) {
          if (!closedSessions.contains(sessionId)) {
              closedSessions.add(sessionId);
  
              currentSession = 0;
  
              // Searching for pending sessions and closing it
              for (PendingSession p : pendingSessions) {
                  if (p.getSessionId() == sessionId) {
                      pendingSessions.remove(p);
                      break;
                  }
              }
  
              // Killing Peer Connection
              if (peerConnection != null) {
                  peerConnection.kill();
                  peerConnection = null;
              }
  
              // Creating new peer connection
              peerConnection = new PeerConnectionInt(
                      iceServers, ownSettings, theirSettings,
                      ownMediaStream, this, context(), self(), "connection/" + (CHILD_NEXT_ID++));
  
              // Pick first pending session if available
              if (pendingSessions.size() > 0) {
                  PendingSession p = pendingSessions.remove(0);
                  if (p != null) {
                      for (Object o : p.getMessages()) {
                          self().sendFirst(o, self());
                      }
                  }
              }
          }
      }
  
      //
      // Stopping
      //
  
      @Override
      public void postStop() {
          if (peerConnection != null) {
              peerConnection.kill();
              peerConnection = null;
          }
          if (ownMediaStream != null) {
              ownMediaStream.release();
              ownMediaStream = null;
          }
          state = PeerState.DISPOSED;
          callback.onPeerStateChanged(deviceId, state);
      }
  
  
      //
      // Messages
      //
  
      @Override
      public void onReceive(Object message) {
          if (message instanceof RTCStart) {
              onEnabled();
          } else if (message instanceof RTCAdvertised) {
              RTCAdvertised advertised = (RTCAdvertised) message;
              onAdvertised(advertised.getSettings());
          } else if (message instanceof ReplaceOwnStream) {
              ReplaceOwnStream ownStream = (ReplaceOwnStream) message;
              replaceOwnStream(ownStream.getMediaStream());
          } else if (message instanceof RTCMasterAdvertised) {
              RTCMasterAdvertised advertisedMaster = (RTCMasterAdvertised) message;
              onMasterAdvertised(advertisedMaster.getIceServers());
          } else if (message instanceof RTCNeedOffer) {
              RTCNeedOffer needOffer = (RTCNeedOffer) message;
              if (peerConnection != null) {
                  if (receiveSessionMessage(needOffer.getSessionId(), message)) {
                      peerConnection.onOfferNeeded(needOffer.getSessionId());
                  }
              } else {
                  stash();
              }
          } else if (message instanceof RTCOffer) {
              RTCOffer offer = (RTCOffer) message;
              if (peerConnection != null) {
                  if (receiveSessionMessage(offer.getSessionId(), message)) {
                      peerConnection.onOffer(offer.getSessionId(), offer.getSdp());
                  }
              } else {
                  stash();
              }
          } else if (message instanceof RTCAnswer) {
              RTCAnswer answer = (RTCAnswer) message;
              if (peerConnection != null) {
                  if (receiveSessionMessage(answer.getSessionId(), message)) {
                      peerConnection.onAnswer(answer.getSessionId(), answer.getSdp());
                  }
              } else {
                  stash();
              }
          } else if (message instanceof RTCCandidate) {
              RTCCandidate candidate = (RTCCandidate) message;
              if (peerConnection != null) {
                  peerConnection.onCandidate(candidate.getSessionId(), candidate.getMdpIndex(), candidate.getId(), candidate.getSdp());
              } else {
                  stash();
              }
          } else if (message instanceof RTCCloseSession) {
              RTCCloseSession closeSession = (RTCCloseSession) message;
              if (peerConnection != null) {
                  onCloseSession(closeSession.getSessionId());
              } else {
                  stash();
              }
          } else if (message instanceof RTCMediaStateUpdated) {
              RTCMediaStateUpdated stateUpdated = (RTCMediaStateUpdated) message;
              onStreamStateChanged(stateUpdated.isAudioEnabled(), stateUpdated.isVideoEnabled());
          } else {
              super.onReceive(message);
          }
      }
  
      @Override
      public Promise onAsk(Object message) throws Exception {
          if (message instanceof ReplaceOwnStream) {
              ReplaceOwnStream ownStream = (ReplaceOwnStream) message;
              return replaceOwnStream(ownStream.getMediaStream());
          } else {
              return super.onAsk(message);
          }
      }
  
      private boolean receiveSessionMessage(long sessionId, Object msg) {
          if (currentSession == sessionId || currentSession == 0) {
              currentSession = sessionId;
              return true;
          } else {
              if (!closedSessions.contains(sessionId)) {
                  boolean found = false;
                  for (PendingSession p : pendingSessions) {
                      if (p.getSessionId() == sessionId) {
                          p.getMessages().add(msg);
                          found = true;
                      }
                  }
                  if (!found) {
                      PendingSession p = new PendingSession(sessionId);
                      p.getMessages().add(msg);
                      pendingSessions.add(p);
                  }
              }
              return false;
          }
      }
  
      public static class ReplaceOwnStream implements AskMessage<Void> {
  
          private CountedReference<WebRTCMediaStream> mediaStream;
  
          public ReplaceOwnStream(CountedReference<WebRTCMediaStream> mediaStream) {
              this.mediaStream = mediaStream;
          }
  
          public CountedReference<WebRTCMediaStream> getMediaStream() {
              return mediaStream;
          }
      }
  
      private class PendingSession {
  
          private long sessionId;
          private ArrayList<Object> messages;
  
          public PendingSession(long sessionId) {
              this.sessionId = sessionId;
              this.messages = new ArrayList<>();
          }
  
          public long getSessionId() {
              return sessionId;
          }
  
          public ArrayList<Object> getMessages() {
              return messages;
          }
      }
  }