use sciforge::hub::prelude::constants::G;
use std::cell::{Cell, RefCell};
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, Command, Stdio};

pub const TRITONMASS: f64 = 2.14e22;
pub const TRITONRADIUS: f64 = 1.3534e6;
pub const NEPTUNETRITONDISTANCE: f64 = 3.548e8;
pub const TRITONORBITALPERIOD: f64 = 507772.8;

pub struct TritonState {
    pub masskg: f64,
    pub radiusm: f64,
    pub semimajoraxism: f64,
    pub orbitalperiods: f64,
    pub eccentricity: f64,
    pub inclinationrad: f64,
    pub raanrad: f64,
    pub argperigeerad: f64,
    pub meananomalyrad: f64,
    pub source: Cell<TritonSource>,
    ipc: RefCell<Option<TritonIpc>>,
}

struct TritonIpc {
    child: Child,
    reader: BufReader<std::process::ChildStdout>,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TritonSource {
    Binary,
    Simulation,
}

fn findtritonbinary() -> Option<std::path::PathBuf> {
    if let Ok(path) = std::env::var("TRITONBINARY") {
        let p = std::path::PathBuf::from(path);
        if p.is_file() {
            return Some(p);
        }
    }
    if let Ok(pathvar) = std::env::var("PATH") {
        for dir in pathvar.split(':') {
            if dir.is_empty() {
                continue;
            }
            let candidate = std::path::Path::new(dir).join("triton");
            if candidate.is_file() {
                return Some(candidate);
            }
        }
    }
    None
}

fn spawntriton(path: &std::path::Path) -> Option<TritonIpc> {
    let mut child = Command::new(path)
        .arg("--ipc")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()
        .ok()?;
    let stdout = child.stdout.take()?;
    let reader = BufReader::new(stdout);
    Some(TritonIpc { child, reader })
}

impl TritonState {
    pub fn new() -> Self {
        if let Some(path) = findtritonbinary()
            && let Some(ipc) = spawntriton(&path)
        {
            return Self {
                masskg: TRITONMASS,
                radiusm: TRITONRADIUS,
                semimajoraxism: NEPTUNETRITONDISTANCE,
                orbitalperiods: TRITONORBITALPERIOD,
                eccentricity: crate::TRITONECCENTRICITY,
                inclinationrad: crate::TRITONINCLINATIONDEG.to_radians(),
                raanrad: 177.0f64.to_radians(),
                argperigeerad: 0.0,
                meananomalyrad: 0.0,
                source: Cell::new(TritonSource::Binary),
                ipc: RefCell::new(Some(ipc)),
            };
        }
        Self::simulated()
    }

    pub fn simulated() -> Self {
        Self {
            masskg: TRITONMASS,
            radiusm: TRITONRADIUS,
            semimajoraxism: NEPTUNETRITONDISTANCE,
            orbitalperiods: TRITONORBITALPERIOD,
            eccentricity: crate::TRITONECCENTRICITY,
            inclinationrad: crate::TRITONINCLINATIONDEG.to_radians(),
            raanrad: 177.0f64.to_radians(),
            argperigeerad: 0.0,
            meananomalyrad: 0.0,
            source: Cell::new(TritonSource::Simulation),
            ipc: RefCell::new(None),
        }
    }

    fn ipcquery(&self, cmd: &str) -> Option<String> {
        let mut ipcborrow = self.ipc.borrow_mut();
        let ipc = ipcborrow.as_mut()?;
        let stdin = ipc.child.stdin.as_mut()?;
        writeln!(stdin, "{}", cmd).ok()?;
        stdin.flush().ok()?;
        let mut line = String::new();
        ipc.reader.read_line(&mut line).ok()?;
        Some(line.trim().to_string())
    }

    fn parsexyz(s: &str) -> Option<(f64, f64, f64)> {
        let parts: Vec<&str> = s.split_whitespace().collect();
        if parts.len() >= 3 {
            let x = parts[0].parse::<f64>().ok()?;
            let y = parts[1].parse::<f64>().ok()?;
            let z = parts[2].parse::<f64>().ok()?;
            Some((x, y, z))
        } else {
            None
        }
    }

    pub fn step(&mut self, dts: f64) {
        if self.source.get() == TritonSource::Binary {
            if let Some(resp) = self.ipcquery(&format!("step {}", dts)) {
                if let Some((x, y, z)) = Self::parsexyz(&resp) {
                    let r = (x * x + y * y + z * z).sqrt();
                    if r > 0.0 {
                        self.semimajoraxism = r;
                    }
                }
                return;
            }
            self.source.set(TritonSource::Simulation);
            *self.ipc.borrow_mut() = None;
        }
        let pi2 = 2.0 * std::f64::consts::PI;
        let n = pi2 / self.orbitalperiods;
        self.meananomalyrad = (self.meananomalyrad + n * dts) % pi2;
        let raanrate = -pi2 / (688.0 * crate::SECONDSPERYEAR);
        self.raanrad = (self.raanrad + raanrate * dts) % pi2;
    }

    fn eccentricanomaly(&self) -> f64 {
        let m = self.meananomalyrad;
        let e = self.eccentricity;
        let mut ea = m + e * m.sin();
        for iter in 0..15 {
            let f = ea - e * ea.sin() - m;
            let fp = 1.0 - e * ea.cos();
            ea -= f / fp;
            if f.abs() < 1e-12 || iter == 14 {
                break;
            }
        }
        ea
    }

    fn trueanomaly(&self) -> f64 {
        let ea = self.eccentricanomaly();
        let e = self.eccentricity;
        2.0 * f64::atan2(
            (1.0 + e).sqrt() * (ea / 2.0).sin(),
            (1.0 - e).sqrt() * (ea / 2.0).cos(),
        )
    }

    pub fn distance(&self) -> f64 {
        if self.source.get() == TritonSource::Binary {
            let (x, y, z) = self.position();
            return (x * x + y * y + z * z).sqrt();
        }
        let nu = self.trueanomaly();
        let e = self.eccentricity;
        self.semimajoraxism * (1.0 - e * e) / (1.0 + e * nu.cos())
    }

    pub fn position(&self) -> (f64, f64, f64) {
        if self.source.get() == TritonSource::Binary
            && let Some(resp) = self.ipcquery("position")
            && let Some((x, y, z)) = Self::parsexyz(&resp)
        {
            return (x, y, z);
        }
        let nu = self.trueanomaly();
        let r = {
            let e = self.eccentricity;
            self.semimajoraxism * (1.0 - e * e) / (1.0 + e * nu.cos())
        };
        let xorb = r * nu.cos();
        let yorb = r * nu.sin();
        let w = self.argperigeerad;
        let xw = xorb * w.cos() - yorb * w.sin();
        let yw = xorb * w.sin() + yorb * w.cos();
        let i = self.inclinationrad;
        let xi = xw;
        let yi = yw * i.cos();
        let zi = yw * i.sin();
        let omega = self.raanrad;
        let x = xi * omega.cos() - yi * omega.sin();
        let y = xi * omega.sin() + yi * omega.cos();
        let z = zi;
        (x, y, z)
    }

    pub fn gravityat(&self, distancem: f64) -> f64 {
        G * self.masskg / (distancem * distancem)
    }

    pub fn isbinary(&self) -> bool {
        self.source.get() == TritonSource::Binary
    }
}

impl Default for TritonState {
    fn default() -> Self {
        Self::new()
    }
}
