package voacap

import (
	"cmp"
	"fmt"
	"io"
	"math"
	"time"

	"github.com/pd0mz/go-maidenhead"
)

// EncodeParams holds the input parameters for a VOACAP prediction.
type EncodeParams struct {
	DateTime           time.Time
	From               string
	To                 string
	SSN                int
	TransmitPower      int
	MinSNR             int
	Frequency          float64 // Frequency in MHz
	MinTakeoffAngle    float64 // Minimum takeoff angle in degrees
	MultipathDelay     float64 // Multipath delay in ms
	MultipathTolerance float64 // Multipath tolerance in dB
	LocalNoise         float64 // Local noise at 3 MHz in -dBw/Hz
	IncludeMUF         bool    // Whether to include MUF in prediction output
}

// Encode writes the input data for a VOACAP prediction to the given writer.
func Encode(w io.Writer, params EncodeParams) error {
	from, err := maidenhead.ParseLocator(params.From)
	if err != nil {
		return fmt.Errorf("error converting 'from' Maidenhead locator: %w", err)
	}
	to, err := maidenhead.ParseLocator(params.To)
	if err != nil {
		return fmt.Errorf("error converting 'to' Maidenhead locator: %w", err)
	}

	// VOACAP expects time in UTC
	params.DateTime = params.DateTime.UTC()

	writeCommentCard(w, fmt.Sprintf("%.1f MHz, %s -> %s @ %s", params.Frequency, params.From, params.To, params.DateTime.Truncate(time.Second).Format(time.RFC3339)))
	writeLineMaxCard(w, 55)
	writeCoeffsCard(w, "CCIR")
	writeTimeCard(w, params.DateTime.Hour(), params.DateTime.Hour(), 1, 1)
	writeMonthCard(w, params.DateTime.Year(), params.DateTime.Month())
	writeSunspotCard(w, params.SSN)
	writeLabelCard(w, params.From, params.To)
	writeCircuitCard(w, from.Latitude, from.Longitude, to.Latitude, to.Longitude, 0)
	// Default values from express-params.md if not specified
	minTakeoffAngle := params.MinTakeoffAngle
	if minTakeoffAngle == 0 {
		minTakeoffAngle = 15.0
	}
	multipathDelay := params.MultipathDelay
	if multipathDelay == 0 {
		multipathDelay = 0.1
	}
	multipathTolerance := params.MultipathTolerance
	if multipathTolerance == 0 {
		multipathTolerance = 0.5
	}
	localNoise := params.LocalNoise
	if localNoise == 0 {
		localNoise = 145.0
	}
	writeSystemCard(w, float64(params.MinSNR), minTakeoffAngle, multipathDelay, multipathTolerance, localNoise)
	writeFprobCard(w)
	writeAntennaCard(w, 1, 1, 2, 30, params.Frequency, "", 90.0, float64(params.TransmitPower))
	writeAntennaCard(w, 2, 2, 2, 30, params.Frequency, "", 270.0, float64(params.TransmitPower))
	writeFrequencyCard(w, params.Frequency)
	writeMethodCard(w, 30, 0)
	writeExecuteCard(w)
	writeQuitCard(w)
	return nil
}

func getLatHemi(lat float64) string {
	if lat < 0 {
		return "S"
	}
	return "N"
}

func getLonHemi(lon float64) string {
	if lon < 0 {
		return "W"
	}
	return "E"
}

func writeCommentCard(w io.Writer, comment string) {
	fmt.Fprintf(w, "COMMENT    %-74s\n", comment)
}

func writeLineMaxCard(w io.Writer, maxLines int) {
	fmt.Fprintf(w, "LINEMAX   %5d\n", maxLines)
}

func writeCoeffsCard(w io.Writer, coeffs string) {
	fmt.Fprintf(w, "COEFFS    %-4s\n", coeffs)
}

func writeTimeCard(w io.Writer, start, end, step, mode int) {
	start, end = cmp.Or(start, 24), cmp.Or(end, 24) // Midnight hour is represented as 24
	fmt.Fprintf(w, "TIME         %5d%5d%5d%5d\n", start, end, step, mode)
}

func writeMonthCard(w io.Writer, year int, month time.Month) {
	fmt.Fprintf(w, "MONTH     %5d%5.2f\n", year, float64(month))
}

func writeSunspotCard(w io.Writer, sunspotNumber int) {
	fmt.Fprintf(w, "SUNSPOT   %5.2f\n", float64(sunspotNumber))
}

func writeLabelCard(w io.Writer, txName, rxName string) {
	fmt.Fprintf(w, "LABEL     %-20s%-20s\n", txName, rxName)
}

func writeCircuitCard(w io.Writer, txLat, txLon, rxLat, rxLon float64, path int) {
	fmt.Fprintf(w, "CIRCUIT   %5.2f%s%9.2f%s%9.2f%s%9.2f%s  S %5d\n",
		math.Abs(txLat), getLatHemi(txLat),
		math.Abs(txLon), getLonHemi(txLon),
		math.Abs(rxLat), getLatHemi(rxLat),
		math.Abs(rxLon), getLonHemi(rxLon),
		path)
}

func writeSystemCard(w io.Writer, requiredSnr, minTakeoffAngle, multipathDelay, multipathTolerance, localNoise float64) {
	// Parameter order according to VOACAP documentation:
	// fixed-factor, local-noise, multipath-delay, min-takeoff-angle, required-SNR, power-tolerance, delay-tolerance
	//
	// NOTE: There appears to be a mismatch in VOACAP where it shows "Minimum Angle= 0.100 degrees" in the output
	// despite us setting minTakeoffAngle to 15.0. This suggests that VOACAP is incorrectly using the
	// multipathDelay value (0.10) as the minimum angle in its output header. However, the actual
	// calculations seem to be using the correct takeoff angle value, as we can see in the TANGLE output rows.
	fmt.Fprintf(w, "SYSTEM    %5.2f%5.0f%5.2f%5.0f%5.2f%5.2f%5.2f\n", 1.0, localNoise, multipathDelay, minTakeoffAngle, requiredSnr, multipathTolerance, multipathDelay)
}

func writeFprobCard(w io.Writer) {
	fmt.Fprintf(w, "FPROB     %5.2f%5.2f%5.2f%5.2f\n", 1.0, 1.0, 1.0, 0.0)
}

func writeAntennaCard(w io.Writer, antType, id, minFreq, maxFreq int, freqMHz float64, antFile string, bearing, powerW float64) {
	powerKW := powerW / 1000.0
	fmt.Fprintf(w, "ANTENNA   %5d%5d%5d%5d%10.3f[%-21s]%5.1f%10.4f\n", antType, id, minFreq, maxFreq, freqMHz, antFile, bearing, powerKW)
}

func writeFrequencyCard(w io.Writer, freqMHz float64) {
	fmt.Fprintf(w, "FREQUENCY %5.3f\n", freqMHz)
}

func writeMethodCard(w io.Writer, method, spec int) {
	fmt.Fprintf(w, "METHOD    %5d%5d\n", method, spec)
}

func writeExecuteCard(w io.Writer) {
	fmt.Fprintln(w, "EXECUTE")
}

func writeQuitCard(w io.Writer) {
	fmt.Fprintln(w, "QUIT")
}
