package de.serosystems.grx.example;

import de.serosystems.proto.v3.grx.receiverd.ReceiverDProto;
import de.serosystems.proto.v3.grx.receiverd.ReceiverdGrpc;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

import java.nio.ShortBuffer;
import java.time.Instant;
import java.util.Arrays;
import java.util.Iterator;

/**
 * Example: Subscribe to Mode S downlink frames with I/Q samples attached.
 *
 * <p>This example connects to the Receiverd service and subscribes to DF 17 (ADS-B)
 * frames with I/Q samples enabled via a {@code SampleEnableRule} using
 * {@code MATCH_ANY_ADDRESS}. It collects 10 frames, prints frame metadata and
 * sample information, and finally dumps the I and Q arrays of the last frame.</p>
 *
 * <p>The I/Q samples use the {@code S16I_S16Q} format: interleaved 16-bit signed
 * integers, I first, then Q.</p>
 *
 * <p>Usage: {@code java IQSamples [host]}</p>
 * <p>Default host: YOUR_GRX_IP</p>
 */
public class IQSamples {

	/** Default IP address of the GRX receiver. */
	private static final String DEFAULT_HOST = "YOUR_GRX_IP";

	/** Receiverd gRPC port. */
	private static final int RECEIVERD_PORT = 5303;

	/** GPS leap seconds as of 2017-01-01 (18 seconds). */
	public static final long CURRENT_GPS_LEAP_NANOSECONDS = 18_000_000_000L;

	/** Number of frames to collect. */
	private static final int FRAME_COUNT = 10;

	/**
	 * Converts a GNSS timestamp (nanoseconds within the GPS week) to a Unix
	 * timestamp in nanoseconds, using a reference time to determine the correct
	 * GPS week.
	 *
	 * @param gnssTimestamp GNSS timestamp in nanoseconds (within GPS week)
	 * @param referenceTime reference Unix time in milliseconds (e.g. System.currentTimeMillis())
	 * @return Unix timestamp in nanoseconds
	 */
	public static long gnssTimestampToUnixtime(long gnssTimestamp, long referenceTime) {
		// Round to start of GPS week (Unix epoch was a Thursday, GPS weeks start on Sunday)
		long startOfWeekMs = Math.floorDiv(referenceTime + 4L * 86_400_000L, 7L * 86_400_000L)
				* 7L * 86_400_000L;
		startOfWeekMs = startOfWeekMs - 4L * 86_400_000L;

		// Handle week rollover: check if GNSS timestamp refers to a different week
		long diff = (referenceTime - startOfWeekMs) * 1_000_000L - gnssTimestamp;
		if (diff < -604740_000_000_000L)
			startOfWeekMs = startOfWeekMs - (7L * 86_400_000L);
		else if (diff > 604740_000_000_000L)
			startOfWeekMs = startOfWeekMs + (7L * 86_400_000L);

		return startOfWeekMs * 1_000_000 + gnssTimestamp - CURRENT_GPS_LEAP_NANOSECONDS;
	}

	public static void main(String[] args) {
		// Use the first CLI argument as hostname, or fall back to the default
		String host = args.length > 0 ? args[0] : DEFAULT_HOST;

		// Create a plaintext gRPC channel to the Receiverd service
		ManagedChannel channel = ManagedChannelBuilder.forAddress(host, RECEIVERD_PORT)
				.usePlaintext()
				.build();

		try {
			// Create a blocking stub for the Receiverd service
			ReceiverdGrpc.ReceiverdBlockingStub stub = ReceiverdGrpc.newBlockingStub(channel);

			// Build a SampleEnableRule that matches any address for DF 17.
			// This tells the receiver to include raw I/Q samples for all DF 17 frames.
			ReceiverDProto.GetModeSDownlinkFramesRequest.SampleEnableRule sampleRule =
					ReceiverDProto.GetModeSDownlinkFramesRequest.SampleEnableRule.newBuilder()
							.setAddress(ReceiverDProto.QualifiedAddressModeS.newBuilder()
									.setAq(ReceiverDProto.QualifiedAddressModeS.AddressQualifier.MATCH_ANY_ADDRESS)
									.setAddress(0)
									.build())
							.addDownlinkFormats(17)
							.build();

			// Build the subscription request for DF 17 with samples enabled
			ReceiverDProto.GetModeSDownlinkFramesRequest request =
					ReceiverDProto.GetModeSDownlinkFramesRequest.newBuilder()
							.addDownlinkFormats(17)
							.addSampleEnableRules(sampleRule)
							.build();

			// Open the blocking stream
			Iterator<ReceiverDProto.ModeSDownlinkFrameWithStreamInfo> frameIterator =
					stub.getModeSDownlinkFrames(request);

			// Collect frames and print details
			int count = 0;
			short[] lastI = null, lastQ = null;

			while (frameIterator.hasNext() && count < FRAME_COUNT) {
				ReceiverDProto.ModeSDownlinkFrameWithStreamInfo msg = frameIterator.next();
				ReceiverDProto.ModeSDownlinkFrame frm = msg.getFrame();
				count++;

				// Convert GNSS timestamp to Unix time for display
				long unixNs = gnssTimestampToUnixtime(frm.getTimestamp(), System.currentTimeMillis());

				// Convert payload to hex
				StringBuilder hex = new StringBuilder();
				for (byte b : frm.getPayload().toByteArray()) {
					hex.append(String.format("%02x", b));
				}

				System.out.printf("Received frame %d:%n", count);
				System.out.printf("\tDropped frames: %d%n", msg.getFramesDropped());
				System.out.printf("\tPayload: %s%n", hex);
				System.out.printf("\tTimestamp: %d ns (%s)%n",
						frm.getTimestamp(), Instant.ofEpochMilli(unixNs / 1_000_000L));

				if (frm.getCarrierFrequencyOffsetComputed()) {
					System.out.printf("\tCarrier freq offset: %.2f Hz (error: %.4f)%n",
							frm.getCarrierFrequencyOffset(),
							frm.getCarrierFrequencyEstimationError());
				}

				System.out.printf("\tSignal/noise level: %.2f dBm / %.2f dBm%n",
						frm.getLevelSignal(), frm.getLevelNoise());
				System.out.printf("\tHas samples: %b%n", frm.hasSamples());

				// Parse I/Q samples if present
				if (frm.hasSamples()) {
					ReceiverDProto.SignalSamples s = frm.getSamples();
					System.out.printf("\tSample rate: %.3f MHz%n", s.getSampleRate() / 1e6);
					System.out.printf("\tStart of data index: %d%n", s.getStartOfData());
					System.out.printf("\tSample format: %s%n", s.getSampleFormat());
					System.out.printf("\tData volume: %d bytes%n", s.getSamples().size());

					// Parse S16I_S16Q: interleaved 16-bit signed I, Q pairs
					if (s.getSampleFormat() == ReceiverDProto.SignalSamples.SampleFormat.S16I_S16Q) {
						ShortBuffer iq = s.getSamples().asReadOnlyByteBuffer().asShortBuffer();
						int n = iq.capacity() / 2;
						System.out.printf("\tNumber of I/Q samples: %d%n", n);
						System.out.printf("\tDuration: %.2f us%n", 1e6 * n / s.getSampleRate());

						// Separate into I and Q arrays
						lastI = new short[n];
						lastQ = new short[n];
						for (int j = 0; j < n; j++) {
							lastI[j] = iq.get(j * 2);
							lastQ[j] = iq.get(j * 2 + 1);
						}
					}
				}
				System.out.println();
			}

			// Print the I/Q samples of the last frame
			if (lastI != null) {
				System.out.println("I/Q samples of the last frame:");
				System.out.printf("I = %s%n", Arrays.toString(lastI));
				System.out.printf("Q = %s%n", Arrays.toString(lastQ));
			}

		} catch (Exception e) {
			System.err.println("Error during I/Q subscription: " + e.getMessage());
			e.printStackTrace();
		} finally {
			// Always shut down the channel to release resources
			channel.shutdownNow();
		}
	}
}
