use super::{BrightnessBackend, BrightnessBackendConstructor};

const EXPECT_STR: &str = "VirtualDevice didn't test the command during initialization";

use anyhow::bail;
use std::{error::Error, process::Command, str::FromStr};
use thiserror::Error;

enum CliArg<'arg> {
	Simple(&'arg str),
	KeyValue { key: &'arg str, value: &'arg str },
}

impl<'arg> From<&'arg str> for CliArg<'arg> {
	fn from(value: &'arg str) -> Self {
		CliArg::Simple(value)
	}
}

impl<'arg> From<(&'arg str, &'arg str)> for CliArg<'arg> {
	fn from((key, value): (&'arg str, &'arg str)) -> Self {
		CliArg::KeyValue { key, value }
	}
}

#[derive(Default)]
struct VirtualDevice {
	name: Option<String>,
	current: Option<u32>,
	max: Option<u32>,
}

#[allow(unused)]
pub(super) struct BrightnessCtl {
	device: VirtualDevice,
}

#[derive(Error, Debug)]
#[error("Requested device '{device_name}' does not exist ")]
pub struct DeviceDoesntExistError {
	device_name: String,
}

#[allow(unused)]
impl VirtualDevice {
	fn try_new(device_name: Option<String>) -> anyhow::Result<Self> {
		let s = Self {
			name: device_name.clone(),
			..Default::default()
		};

		// Check if the command is available to us before running it in other occasions
		let exit_code = s.command(CliArg::Simple("info")).output()?.status;

		if exit_code.success() {
			Ok(s)
		} else {
			bail!(DeviceDoesntExistError {
				device_name: device_name.unwrap_or("Device name unknown".to_string())
			})
		}
	}

	fn command(&self, arg: CliArg) -> Command {
		let mut cmd = Command::new("brightnessctl");

		if let Some(name) = &self.name {
			cmd.arg("--device").arg(name);
		}

		match arg {
			CliArg::Simple(arg) => cmd.arg(arg),
			CliArg::KeyValue { key, value } => cmd.arg(key).arg(value),
		};

		cmd
	}

	fn run<'arg, T: FromStr, A: Into<CliArg<'arg>>>(&self, arg: A) -> anyhow::Result<T>
	where
		<T as FromStr>::Err: Error + Send + Sync + 'static,
	{
		let cmd_output = self.command(arg.into()).output()?.stdout;

		let cmd_output = String::from_utf8_lossy(&cmd_output);

		Ok(cmd_output.trim().parse()?)
	}

	fn get_current(&mut self) -> u32 {
		match self.current {
			Some(val) => val,
			None => {
				let val = self.run("get").expect(EXPECT_STR);
				self.current = Some(val);
				val
			}
		}
	}

	fn get_max(&mut self) -> u32 {
		match self.max {
			Some(val) => val,
			None => {
				let val = self.run("max").expect(EXPECT_STR);
				self.max = Some(val);
				val
			}
		}
	}

	fn set_raw(&mut self, val: u32) -> anyhow::Result<()> {
		let max = self.get_max();
		let clamped_val = val.clamp(0, max);
		self.current = Some(clamped_val);
		let _: String = self.run(("set", &*format!("{clamped_val}")))?; // No % sign here
		Ok(())
	}

	fn set_percent(&mut self, mut val: u32) -> anyhow::Result<()> {
		val = val.clamp(0, 100);
		self.current = self.max.map(|max| div_round_u32(val * max, 100));
		let _: String = self.run(("set", &*format!("{val}%")))?;
		Ok(())
	}

	pub fn get_percent(&mut self) -> u32 {
		let curr = self.get_current();
		let max = self.get_max();
		div_round_u32(curr * 100, max)
	}
}

impl BrightnessBackendConstructor for BrightnessCtl {
	fn try_new(device_name: Option<String>) -> anyhow::Result<Self> {
		Ok(Self {
			device: VirtualDevice::try_new(device_name)?,
		})
	}
}

impl BrightnessBackend for BrightnessCtl {
	fn get_current(&mut self) -> u32 {
		self.device.get_current()
	}

	fn get_max(&mut self) -> u32 {
		self.device.get_max()
	}

	fn lower(&mut self, by: u32, min: u32) -> anyhow::Result<()> {
		let max = self.device.get_max();
		let curr = self.device.get_current();
		let step = div_round_u32(by * max, 100);
		let new_val = curr.saturating_sub(step);
		let min_raw = div_round_u32(min * max, 100);
		self.device.set_raw(new_val.max(min_raw))
	}

	fn raise(&mut self, by: u32, min: u32) -> anyhow::Result<()> {
		let max = self.device.get_max();
		let curr = self.device.get_current();
		let step = div_round_u32(by * max, 100);
		let new_val = (curr + step).min(max);
		let min_raw = div_round_u32(min * max, 100);
		self.device.set_raw(new_val.max(min_raw))
	}

	fn set(&mut self, val: u32, min: u32) -> anyhow::Result<()> {
		let max = self.device.get_max();
		let raw_val = div_round_u32(val.max(min) * max, 100);
		self.device.set_raw(raw_val)
	}
}

fn div_round_u32(a: u32, b: u32) -> u32 {
	(a + b / 2) / b
}
