package com.foxx.androidcast.sender;

/*********************************************************************
 * MultiCastCoordinator.java
 * Created at: Sat 16 May 2026 13:35:48 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class MultiCastCoordinator
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (8 commits, 271 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	36a73cee00f829a6ec66f4f6ab1d178d6ec37263026bc09f99f86d881f45462e
**********************************************************************/
import android.content.Context;
import android.util.Log;

import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.DeviceInfo;
import com.foxx.androidcast.media.AudioNegotiator;
import com.foxx.androidcast.network.CastCodecFlags;
import com.foxx.androidcast.network.CastFramingContext;
import com.foxx.androidcast.network.CastProtocol;
import com.foxx.androidcast.network.CastSession;
import com.foxx.androidcast.network.CastTransport;
import com.foxx.androidcast.network.CastTransportFactory;
import com.foxx.androidcast.network.transport.ProtectionNegotiationPrompts;
import com.foxx.androidcast.network.transport.StreamProtectionNegotiation;
import com.foxx.androidcast.network.UdpCastTransport;
import com.foxx.androidcast.network.quic.QuicCronetCastTransport;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Manages multiple receiver sessions (connect, handshake, fan-out send).
 * Requests keyframes when a peer joins or changes transport.
 */
public final class MultiCastCoordinator {
  private static final String TAG = "MultiCastCoordinator";
  private static final int CONNECT_RETRIES = 20;

  public interface KeyframeRequester {
    void requestKeyframe();
  }

  public static final class ActivePeer {
    public final CastReceiverTarget target;
    public final CastSession session;
    public CastSettings settings;
    public String negotiatedMime;

    ActivePeer(CastReceiverTarget target, CastSession session, CastSettings settings, String mime) {
      this.target = target;
      this.session = session;
      this.settings = settings;
      this.negotiatedMime = mime;
    }
  }

  private final Context appContext;
  private final int sessionId;
  private final CastFramingContext framingContext;
  private final CopyOnWriteArrayList<ActivePeer> peers = new CopyOnWriteArrayList<>();
  private KeyframeRequester keyframeRequester;

  public MultiCastCoordinator(Context context) {
    appContext = context.getApplicationContext();
    sessionId = new Random().nextInt();
    int placeholderId = DeviceInfo.getPlaceholderId(appContext);
    framingContext = new CastFramingContext(
        sessionId,
        placeholderId,
        CastCodecFlags.localVideoMask(),
        CastCodecFlags.localAudioMask());
  }

  public int getSessionId() {
    return sessionId;
  }

  public CastFramingContext getFramingContext() {
    return framingContext;
  }

  public void setKeyframeRequester(KeyframeRequester requester) {
    this.keyframeRequester = requester;
  }

  public List<ActivePeer> getPeers() {
    return new ArrayList<>(peers);
  }

  public List<CastSession> getSessions() {
    List<CastSession> out = new ArrayList<>();
    for (ActivePeer p : peers) {
      out.add(p.session);
    }
    return out;
  }

  public void connectAndHandshake(List<CastReceiverTarget> targets, CastSettings baseSettings,
      String deviceName, String pin) throws IOException {
    peers.clear();
    IOException last = null;
    for (CastReceiverTarget t : targets) {
      CastSettings peerSettings = cloneSettings(baseSettings);
      peerSettings.setTransport(t.transport);
      CastSession session = null;
      try {
        session = createSession(peerSettings);
        connectWithRetry(session, t.host, t.port);
        CastSession.HandshakeResult hs = session.clientHandshake(deviceName, pin, peerSettings);
        applyNegotiatedProtection(session, peerSettings, hs.receiverAdvertisement);
        AudioNegotiator.resolveForSender(peerSettings, t.audioCodecs);
        peers.add(new ActivePeer(t, session, hs.settings, hs.negotiatedVideoMime));
        Log.i(TAG, "Peer ready " + t.name + " " + hs.negotiatedVideoMime
                + " audio=" + peerSettings.getResolvedAudioCodec()
                + " score="
                + com.foxx.androidcast.media.CodecNegotiator.priorityScoreForNegotiatedMime(
                        hs.negotiatedVideoMime, hs.settings));
      } catch (IOException e) {
        last = e;
        Log.w(TAG, "Peer failed " + t.name + ": " + e.getMessage());
        if (session != null) {
          session.close();
        }
      }
    }
    if (peers.isEmpty() && last != null) {
      throw last;
    }
    if (keyframeRequester != null) {
      keyframeRequester.requestKeyframe();
    }
  }

  public void addPeer(CastReceiverTarget target, CastSettings baseSettings, String deviceName, String pin)
      throws IOException {
    CastSettings peerSettings = cloneSettings(baseSettings);
    peerSettings.setTransport(target.transport);
    CastSession session = createSession(peerSettings);
    connectWithRetry(session, target.host, target.port);
    CastSession.HandshakeResult hs = session.clientHandshake(deviceName, pin, peerSettings);
    applyNegotiatedProtection(session, peerSettings, hs.receiverAdvertisement);
    AudioNegotiator.resolveForSender(peerSettings, target.audioCodecs);
    peers.add(new ActivePeer(target, session, hs.settings, hs.negotiatedVideoMime));
    if (keyframeRequester != null) {
      keyframeRequester.requestKeyframe();
    }
  }

  public void removePeer(String host, int port) {
    String key = host + ":" + port;
    for (ActivePeer p : peers) {
      if (p.target.key().equals(key)) {
        peers.remove(p);
        p.session.close();
        break;
      }
    }
  }

  public CastProtocol.Message pollReceive(int timeoutMs) throws IOException {
    if (peers.isEmpty()) {
      return null;
    }
    long deadline = System.currentTimeMillis() + timeoutMs;
    CastProtocol.Message any = null;
    for (ActivePeer p : peers) {
      long remaining = deadline - System.currentTimeMillis();
      if (remaining <= 0) {
        break;
      }
      CastProtocol.Message msg = p.session.receive((int) remaining);
      if (msg != null) {
        any = msg;
      }
    }
    return any;
  }

  /** Sends the same control message to every connected peer (e.g. stream config after pipeline ready). */
  public void broadcast(byte type, byte[] payload) {
    for (ActivePeer p : peers) {
      try {
        p.session.send(type, payload);
      } catch (IOException e) {
        Log.w(TAG, "Broadcast to " + p.target.name + " failed: " + e.getMessage());
      }
    }
  }

  public void sendGoodbyeAll() {
    for (ActivePeer p : peers) {
      try {
        p.session.send(CastProtocol.MSG_GOODBYE, new byte[0]);
      } catch (IOException ignored) {
      }
    }
  }

  public void closeAll() {
    for (ActivePeer p : peers) {
      try {
        p.session.close();
      } catch (Exception ignored) {
      }
    }
    peers.clear();
  }

  private CastSession createSession(CastSettings settings) throws IOException {
    CastTransportFactory.init(appContext);
    CastSession session = new CastSession(CastTransportFactory.create(settings));
    session.getTransport().setFramingContext(framingContext);
    return session;
  }

  private void applyNegotiatedProtection(CastSession session, CastSettings local,
      CastSettings receiverAdvertisement) {
    if (session == null) {
      return;
    }
    CastTransport transport = session.getTransport();
    UdpCastTransport udp = null;
    if (transport instanceof UdpCastTransport) {
      udp = (UdpCastTransport) transport;
    } else if (transport instanceof QuicCronetCastTransport) {
      udp = ((QuicCronetCastTransport) transport).getUdpTransport();
    }
    if (udp != null) {
      CastSettings remote = receiverAdvertisement;
      StreamProtectionNegotiation negotiated = CastTransportFactory.applyNegotiatedUdpProtection(
              udp, local, remote, false);
      Log.i(TAG, ProtectionNegotiationPrompts.summarize(appContext, negotiated));
      if (negotiated.prompt != null) {
        Log.i(TAG, ProtectionNegotiationPrompts.format(appContext, negotiated.prompt, false));
      }
    }
  }

  private void connectWithRetry(CastSession session, String host, int port) throws IOException {
    IOException last = null;
    for (int i = 0; i < CONNECT_RETRIES; i++) {
      try {
        session.connect(host, port);
        return;
      } catch (IOException e) {
        last = e;
        try {
          Thread.sleep(1000);
        } catch (InterruptedException ie) {
          Thread.currentThread().interrupt();
          throw new IOException("Interrupted", ie);
        }
      }
    }
    throw last != null ? last : new IOException("Connect failed");
  }

  private static CastSettings cloneSettings(CastSettings src) {
    CastSettings s = new CastSettings();
    s.setTransport(src.getTransport());
    s.setQuality(src.getQuality());
    s.setResolution(src.getResolution());
    s.setAudioEnabled(src.isAudioEnabled());
    s.setVideoCodec(src.getVideoCodec());
    s.setAdjustmentPreset(src.getAdjustmentPreset());
    s.setBitrateMode(src.getBitrateMode());
    s.setNetworkAdoption(src.getNetworkAdoption());
    s.setCaptureMode(src.getCaptureMode());
    s.setStreamProtection(src.getStreamProtection());
    s.setReceiverDisplayResolution(src.getReceiverDisplayResolution());
    return s;
  }
}
