content: /*
   * Copyright (C) 2015 Actor LLC. <https://actor.im>
   */
  
  package im.actor.core.network.mtp.actors;
  
  import com.google.j2objc.annotations.AutoreleasePool;
  
  import java.io.IOException;
  
  import im.actor.core.network.ActorApi;
  import im.actor.core.network.Endpoints;
  import im.actor.core.network.NetworkState;
  import im.actor.core.network.mtp.MTProto;
  import im.actor.core.network.mtp.entity.EncryptedCBCPackage;
  import im.actor.core.network.mtp.entity.EncryptedPackage;
  import im.actor.core.network.mtp.entity.ProtoMessage;
  import im.actor.runtime.util.ExponentialBackoff;
  import im.actor.runtime.*;
  import im.actor.runtime.actors.Actor;
  import im.actor.runtime.actors.ActorRef;
  import im.actor.runtime.actors.ActorSelection;
  import im.actor.runtime.actors.ActorSystem;
  import im.actor.runtime.actors.ActorCancellable;
  import im.actor.runtime.actors.Props;
  import im.actor.runtime.bser.DataInput;
  import im.actor.runtime.bser.DataOutput;
  import im.actor.runtime.crypto.ActorProtoKey;
  import im.actor.runtime.crypto.box.CBCHmacBox;
  import im.actor.runtime.crypto.primitives.kuznechik.KuznechikFastEngine;
  import im.actor.runtime.crypto.primitives.streebog.Streebog256;
  import im.actor.runtime.crypto.primitives.util.ByteStrings;
  import im.actor.runtime.mtproto.Connection;
  import im.actor.runtime.mtproto.ConnectionCallback;
  import im.actor.runtime.mtproto.CreateConnectionCallback;
  import im.actor.runtime.threading.AtomicIntegerCompat;
  
  /**
   * Possible problems
   * * Creating connections after actor kill
   */
  public class ManagerActor extends Actor {
  
      private static final String TAG = "Manager";
  
      public static ActorRef manager(final MTProto mtProto) {
          return ActorSystem.system().actorOf(
                  new ActorSelection(Props.create(() -> new ManagerActor(mtProto)).changeDispatcher("network_manager"), mtProto.getActorPath() + "/manager"));
      }
  
      private static final AtomicIntegerCompat NEXT_CONNECTION = im.actor.runtime.Runtime.createAtomicInt(1);
  
      private final MTProto mtProto;
      private final Endpoints endpoints;
      private final long authId;
      private final byte[] authKey;
      private final ActorProtoKey authProtoKey;
      private final CBCHmacBox serverUSDecryptor;
      private final CBCHmacBox serverRUDecryptor;
      private final CBCHmacBox clientUSEncryptor;
      private final CBCHmacBox clientRUEncryptor;
      private final long sessionId;
      private final boolean isEnableLog;
  
      // Connection
      private int currentConnectionId;
      private Connection currentConnection;
      private NetworkState networkState = NetworkState.UNKNOWN;
      private int outSeq = 0;
      private int inSeq = 0;
  
      // Creating
      private boolean isCheckingConnections = false;
      private final ExponentialBackoff backoff;
      private ActorCancellable checkCancellable;
  
      private ActorRef receiver;
      private ActorRef sender;
  
      public ManagerActor(MTProto mtProto) {
          this.mtProto = mtProto;
          this.endpoints = mtProto.getEndpoints();
          this.authId = mtProto.getAuthId();
          this.authKey = mtProto.getAuthKey();
          if (this.authKey != null) {
              this.authProtoKey = new ActorProtoKey(this.authKey);
              this.serverUSDecryptor = new CBCHmacBox(
                      Crypto.createAES128(this.authProtoKey.getServerKey()),
                      Crypto.createSHA256(),
                      this.authProtoKey.getServerMacKey());
              this.serverRUDecryptor = new CBCHmacBox(
                      new KuznechikFastEngine(this.authProtoKey.getServerRussianKey()),
                      new Streebog256(),
                      this.authProtoKey.getServerMacRussianKey());
              this.clientUSEncryptor = new CBCHmacBox(
                      Crypto.createAES128(this.authProtoKey.getClientKey()),
                      Crypto.createSHA256(),
                      this.authProtoKey.getClientMacKey());
              this.clientRUEncryptor = new CBCHmacBox(
                      new KuznechikFastEngine(this.authProtoKey.getClientRussianKey()),
                      new Streebog256(),
                      this.authProtoKey.getClientMacRussianKey());
          } else {
              this.authProtoKey = null;
              this.serverUSDecryptor = null;
              this.serverRUDecryptor = null;
              this.clientUSEncryptor = null;
              this.clientRUEncryptor = null;
          }
          this.sessionId = mtProto.getSessionId();
          this.isEnableLog = mtProto.isEnableLog();
          backoff = new ExponentialBackoff(mtProto.getMinDelay(), mtProto.getMaxDelay(), mtProto.getMaxFailureCount());
      }
  
      @Override
      public void preStart() {
          receiver = ReceiverActor.receiver(mtProto);
          sender = PusherActor.senderActor(mtProto);
          connectionStateChanged();
          checkConnection();
      }
  
      @Override
      public void postStop() {
          this.receiver = null;
          this.sender = null;
          currentConnectionId = -1;
          if (currentConnection != null) {
              currentConnection.close();
              currentConnection = null;
          }
          connectionStateChanged();
      }
  
      @Override
      public void onReceive(Object message) {
  
          // Connections
          if (message instanceof ConnectionCreated) {
              ConnectionCreated c = (ConnectionCreated) message;
              onConnectionCreated(c.connectionId, c.connection);
          } else if (message instanceof ConnectionCreateFailure) {
              onConnectionCreateFailure();
          } else if (message instanceof ConnectionDie) {
              onConnectionDie(((ConnectionDie) message).connectionId);
          } else if (message instanceof PerformConnectionCheck) {
              checkConnection();
          } else if (message instanceof NetworkChanged) {
              onNetworkChanged(((NetworkChanged) message).state);
          } else if (message instanceof ForceNetworkCheck) {
              forceNetworkCheck();
          }
          // Messages
          else if (message instanceof OutMessage) {
              OutMessage m = (OutMessage) message;
              onOutMessage(m.message, m.offset, m.len);
          } else if (message instanceof InMessage) {
              InMessage m = (InMessage) message;
              onInMessage(m.data, m.offset, m.len);
          } else {
              super.onReceive(message);
          }
      }
  
      private void onConnectionCreated(int id, Connection connection) {
  
          if (connection.isClosed()) {
              if (isEnableLog) {
                  Log.w(TAG, "Unable to register connection #" + id + ": already closed");
              }
              return;
          }
  
          if (currentConnectionId == id) {
              if (isEnableLog) {
                  Log.w(TAG, "Unable to register connection #" + id + ": already have connection");
              }
              return;
          }
  
          Log.d(TAG, "Connection #" + id + " created");
  
          if (currentConnection != null) {
              currentConnection.close();
              currentConnectionId = 0;
          }
  
          currentConnectionId = id;
          currentConnection = connection;
          outSeq = 0;
          inSeq = 0;
          connectionStateChanged();
  
  
          backoff.onSuccess();
          isCheckingConnections = false;
          requestCheckConnection();
  
          sender.send(new PusherActor.ConnectionCreated());
      }
  
      private void onConnectionCreateFailure() {
          Log.w(TAG, "Connection create failure");
  
          backoff.onFailure();
          isCheckingConnections = false;
          requestCheckConnection(backoff.exponentialWait());
      }
  
      private void onConnectionDie(int id) {
          Log.w(TAG, "Connection #" + id + " dies");
  
          if (currentConnectionId == id) {
              currentConnectionId = 0;
              currentConnection = null;
              outSeq = 0;
              inSeq = 0;
              connectionStateChanged();
              requestCheckConnection();
          }
      }
  
      private void onNetworkChanged(NetworkState state) {
          Log.w(TAG, "Network configuration changed: " + state);
          this.networkState = state;
          backoff.reset();
          checkConnection();
      }
  
      private void forceNetworkCheck() {
          if (currentConnection != null) {
              currentConnection.checkConnection();
          }
      }
  
      private void requestCheckConnection() {
          requestCheckConnection(0);
      }
  
      private void requestCheckConnection(long wait) {
          if (!isCheckingConnections) {
              if (currentConnection == null) {
                  if (wait == 0) {
                      Log.w(TAG, "Requesting connection creating");
                  } else {
                      Log.w(TAG, "Requesting connection creating in " + wait + " ms");
                  }
              }
              if (checkCancellable != null) {
                  checkCancellable.cancel();
                  checkCancellable = null;
              }
              checkCancellable = schedule(new PerformConnectionCheck(), wait);
          }
      }
  
      private void checkConnection() {
          if (isCheckingConnections) {
              return;
          }
  
          if (currentConnection == null) {
              if (networkState == NetworkState.NO_CONNECTION) {
                  Log.d(TAG, "Not trying to create connection: Not network available");
                  return;
              }
              Log.d(TAG, "Trying to create connection...");
  
              isCheckingConnections = true;
  
              final int id = NEXT_CONNECTION.getAndIncrement();
  
              Network.createConnection(id, ActorApi.MTPROTO_VERSION,
                      ActorApi.API_MAJOR_VERSION,
                      ActorApi.API_MINOR_VERSION,
                      endpoints.fetchEndpoint(authKey == null), new ConnectionCallback() {
  
                          @Override
                          public void onConnectionRedirect(String host, int port, int timeout) {
                              // TODO: Implement better processing
                              self().send(new ConnectionDie(id));
                          }
  
                          @Override
                          public void onMessage(byte[] data, int offset, int len) {
                              self().send(new InMessage(data, offset, len));
                          }
  
                          @Override
                          public void onConnectionDie() {
                              self().send(new ConnectionDie(id));
                          }
                      }, new CreateConnectionCallback() {
                          @Override
                          public void onConnectionCreated(Connection connection) {
                              self().send(new ConnectionCreated(id, connection));
                          }
  
                          @Override
                          public void onConnectionCreateError() {
                              self().send(new ConnectionCreateFailure());
                          }
                      });
          }
      }
  
      private void connectionStateChanged() {
          mtProto.getCallback().onConnectionsCountChanged(currentConnection != null ? 1 : 0);
      }
  
      @AutoreleasePool
      private void onInMessage(byte[] data, int offset, int len) {
          // Log.d(TAG, "Received package");
  
          DataInput bis = new DataInput(data, offset, len);
          try {
              long authId = bis.readLong();
              long sessionId = bis.readLong();
  
              if (authId != this.authId || sessionId != this.sessionId) {
                  throw new IOException("Incorrect header");
              }
  
              if (authKey != null) {
                  EncryptedPackage encryptedPackage = new EncryptedPackage(bis);
                  int seq = (int) encryptedPackage.getSeqNumber();
                  if (seq != inSeq) {
                      throw new IOException("Expected " + inSeq + ", got: " + seq);
                  }
                  inSeq++;
                  // long start = Runtime.getActorTime();
                  EncryptedCBCPackage usEncryptedPackage = new EncryptedCBCPackage(new DataInput(encryptedPackage.getEncryptedPackage()));
                  byte[] ruPackage = serverUSDecryptor.decryptPackage(ByteStrings.longToBytes(seq), usEncryptedPackage.getIv(), usEncryptedPackage.getEncryptedContent());
                  EncryptedCBCPackage ruEncryptedPackage = new EncryptedCBCPackage(new DataInput(ruPackage));
                  byte[] plainText = serverRUDecryptor.decryptPackage(ByteStrings.longToBytes(seq), ruEncryptedPackage.getIv(), ruEncryptedPackage.getEncryptedContent());
  
                  // Log.d(TAG, "Package decrypted in " + (Runtime.getActorTime() - start) + " ms, size: " + len);
  
                  DataInput ptInput = new DataInput(plainText);
                  long messageId = ptInput.readLong();
                  byte[] ptPayload = ptInput.readProtoBytes();
                  receiver.send(new ProtoMessage(messageId, ptPayload));
              } else {
                  long messageId = bis.readLong();
                  byte[] payload = bis.readProtoBytes();
                  receiver.send(new ProtoMessage(messageId, payload));
              }
          } catch (IOException e) {
              Log.w(TAG, "Closing connection: incorrect package");
              Log.e(TAG, e);
  
              if (currentConnection != null) {
                  try {
                      currentConnection.close();
                  } catch (Exception e2) {
                      e2.printStackTrace();
                  }
                  currentConnection = null;
                  currentConnectionId = 0;
                  outSeq = 0;
                  inSeq = 0;
                  // Log.d(TAG, "Set connection #" + 0);
              }
              checkConnection();
          }
      }
  
      private void onOutMessage(byte[] data, int offset, int len) {
          // Log.d(TAG, "Out message");
  
          // Cleanup bad connection
          if (currentConnection != null && currentConnection.isClosed()) {
              currentConnection = null;
              // Log.d(TAG, "Set connection #" + 0);
              currentConnectionId = 0;
              outSeq = 0;
              inSeq = 0;
              checkConnection();
          }
  
          try {
              if (currentConnection != null) {
                  if (authKey != null) {
                      int seq = outSeq++;
                      // long start = Runtime.getActorTime();
                      byte[] ruIv = new byte[16];
                      Crypto.nextBytes(ruIv);
                      byte[] usIv = new byte[16];
                      Crypto.nextBytes(usIv);
  
                      byte[] ruCipherText = clientRUEncryptor.encryptPackage(ByteStrings.longToBytes(seq), ruIv, ByteStrings.substring(data, offset, len));
                      byte[] ruPackage = new EncryptedCBCPackage(ruIv, ruCipherText).toByteArray();
                      byte[] usCipherText = clientUSEncryptor.encryptPackage(ByteStrings.longToBytes(seq), usIv, ruPackage);
                      byte[] usPackage = new EncryptedCBCPackage(usIv, usCipherText).toByteArray();
  
                      EncryptedPackage encryptedPackage = new EncryptedPackage(seq, usPackage);
                      byte[] cipherData = encryptedPackage.toByteArray();
                      DataOutput bos = new DataOutput();
                      bos.writeLong(authId);
                      bos.writeLong(sessionId);
                      bos.writeBytes(cipherData, 0, cipherData.length);
                      byte[] pkg = bos.toByteArray();
                      currentConnection.post(pkg, 0, pkg.length);
  
                      // Log.d(TAG, "Package encrypted in " + (Runtime.getActorTime() - start) + " ms, size: " + len);
                  } else {
                      DataOutput bos = new DataOutput();
                      bos.writeLong(authId);
                      bos.writeLong(sessionId);
                      bos.writeBytes(data, offset, len);
                      byte[] pkg = bos.toByteArray();
                      currentConnection.post(pkg, 0, pkg.length);
                  }
                  // Log.d(TAG, "Posted message to connection #" + currentConnectionId);
              }
          } catch (IOException e) {
              Log.w(TAG, "Closing connection: exception during push");
              Log.e(TAG, e);
  
              if (currentConnection != null) {
                  try {
                      currentConnection.close();
                  } catch (Exception e2) {
                      e2.printStackTrace();
                  }
                  currentConnection = null;
                  currentConnectionId = 0;
                  outSeq = 0;
                  inSeq = 0;
                  // Log.d(TAG, "Set connection #" + 0);
              }
              checkConnection();
          }
      }
  
      public static class OutMessage {
          private byte[] message;
          private int offset;
          private int len;
  
          public OutMessage(byte[] message, int offset, int len) {
              this.message = message;
              this.offset = offset;
              this.len = len;
          }
  
          public int getOffset() {
              return offset;
          }
  
          public int getLen() {
              return len;
          }
  
          public byte[] getMessage() {
              return message;
          }
      }
  
      public static class InMessage {
          private byte[] data;
          private int offset;
          private int len;
  
          public InMessage(byte[] data, int offset, int len) {
              this.data = data;
              this.offset = offset;
              this.len = len;
          }
  
          public byte[] getData() {
              return data;
          }
  
          public int getOffset() {
              return offset;
          }
  
          public int getLen() {
              return len;
          }
      }
  
      public static class NetworkChanged {
          private NetworkState state;
  
          public NetworkChanged(NetworkState state) {
              this.state = state;
          }
      }
  
      public static class ForceNetworkCheck {
  
      }
  
      private static class PerformConnectionCheck {
  
      }
  
      private static class ConnectionDie {
          private int connectionId;
  
          public ConnectionDie(int connectionId) {
              this.connectionId = connectionId;
          }
  
          public int getConnectionId() {
              return connectionId;
          }
      }
  
      private static class ConnectionCreateFailure {
  
      }
  
      private static class ConnectionCreated {
          private int connectionId;
          private Connection connection;
  
          public ConnectionCreated(int connectionId, Connection connection) {
              this.connectionId = connectionId;
              this.connection = connection;
          }
  
          public int getConnectionId() {
              return connectionId;
          }
  
          public Connection getConnection() {
              return connection;
          }
      }
  }
  