// output.rs — 完整替换

use std::fs;
use std::process::{Command, Child};

// ────────────────────── Source ──────────────────────

#[derive(Clone, Copy, PartialEq)]
pub enum Source {
    User,
    Inotify,
}

// ────────────────────── 共享模板引擎 ──────────────────────

fn format_value(value: f64) -> String {
    if value.fract() == 0.0 {
        format!("{}", value as i64)
    } else {
        let s = format!("{:.4}", value);
        s.trim_end_matches('0').trim_end_matches('.').to_string()
    }
}

fn apply_template(template: &str, value: f64, min: f64, max: f64) -> String {
    let val_str = format_value(value);
    let ratio = if max > min {
        ((value - min) / (max - min)).clamp(0.0, 1.0)
    } else { 0.0 };
    let percent = (ratio * 100.0).round() as i64;

    template
        .replace("{value}", &val_str)
        .replace("{}", &val_str)
        .replace("{min}", &format_value(min))
        .replace("{max}", &format_value(max))
        .replace("{ratio}", &format!("{:.4}", ratio))
        .replace("{percent}", &percent.to_string())
}

// ────────────────────── Output trait ──────────────────────

pub trait Output {
    fn write(&mut self, value: f64, source: Source);
}

// ────────────────────── FileOutput ──────────────────────

pub struct FileOutput {
    path: String,
    template: Option<String>,
    min: f64,
    max: f64,
    watched: bool,
}

impl FileOutput {
    pub fn new(path: String, template: Option<String>, min: f64, max: f64, watched: bool) -> Self {
        Self { path, template, min, max, watched }
    }
}

impl Output for FileOutput {
    fn write(&mut self, value: f64, source: Source) {
        if self.watched && source == Source::Inotify {
            return;
        }
        let content = match &self.template {
            Some(tpl) => apply_template(tpl, value, self.min, self.max),
            None => format_value(value),
        };
        let _ = fs::write(&self.path, format!("{}\n", content));
    }
}

// ────────────────────── CommandOutput ──────────────────────

pub struct CommandOutput {
    cmd_template: String,
    children: Vec<Child>,
    min: f64,
    max: f64,
}

impl CommandOutput {
    pub fn new(cmd_template: String, min: f64, max: f64) -> Self {
        Self { cmd_template, children: Vec::new(), min, max }
    }
}

impl Output for CommandOutput {
    fn write(&mut self, value: f64, source: Source) {
        if source == Source::Inotify {
            return;
        }
        let cmd_str = apply_template(&self.cmd_template, value, self.min, self.max);

        self.children.retain_mut(|c| {
            match c.try_wait() {
                Ok(Some(_)) => false,
                _ => true,
            }
        });

        if let Ok(child) = Command::new("sh").arg("-c").arg(&cmd_str).spawn() {
            self.children.push(child);
        }
    }
}

// ───────────────────── MultiOutput ──────────────────────

pub struct MultiOutput {
    pub outputs: Vec<Box<dyn Output>>,
}

impl Output for MultiOutput {
    fn write(&mut self, value: f64, source: Source) {
        for o in &mut self.outputs {
            o.write(value, source);
        }
    }
}

// ────────────────────── NoopOutput ──────────────────────

pub struct NoopOutput;

impl Output for NoopOutput {
    fn write(&mut self, _value: f64, _source: Source) {}
}
