use std::fmt::Display;
use std::path::{Path, PathBuf};

use cpclib_common::itertools::Itertools;
use serde::de::Visitor;
use serde::{self, Deserialize, Deserializer};

use crate::constraints::{deserialize_constraint, Constraint};
use crate::expand_glob;
use crate::task::Task;

fn deserialize_path_list<'de, D>(deserializer: D) -> Result<Vec<PathBuf>, D::Error>
where D: Deserializer<'de> {
    let s = String::deserialize(deserializer)?;
    let r = shlex::split(&s).or(Some(vec![])).unwrap();
    let r = r
        .into_iter()
        .map(|s| expand_glob(s.as_ref()))
        .flatten()
        .map(|s| {
            if s.starts_with(r"./") || s.starts_with(r".\") {
                s[2..].to_owned()
            }
            else {
                s
            }
        })
        .map(|s| PathBuf::from(s))
        .collect_vec();

    Ok(r)
}

impl Display for Rule {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let targets = self
            .targets()
            .iter()
            .map(|p| p.display().to_string())
            .join(" ");

        writeln!(f, "\n- tgt: {targets}")?;

        if !self.dependencies.is_empty() {
            let dependencies = self
                .dependencies()
                .iter()
                .map(|p| p.display().to_string())
                .join(" ");
            writeln!(f, "  dep: {dependencies}")?;
        }

        if let Some(phony) = &self.phony {
            writeln!(f, "  phony: {}", phony)?;
        }

        if let Some(help) = &self.help {
            writeln!(f, "  help: {}", help)?;
        }

        if let Some(constraint) = &self.constraint {
            writeln!(f, "  constraint: {}", constraint)?;
        }

        if self.commands.len() == 1 {
            writeln!(f, "  cmd: {}", &self.commands[0])?;
        }
        else if !self.commands.is_empty() {
            writeln!(f, "  cmd:")?;
            for cmd in self.commands() {
                writeln!(f, "       - {cmd}")?;
            }
        }

        Ok(())
    }
}

#[derive(Debug, Deserialize, PartialEq, Eq, Hash)]
pub struct Rule {
    /// List of files generated by this rule
    #[serde(deserialize_with = "deserialize_path_list")]
    #[serde(alias = "tgt", alias = "target", alias = "build")]
    targets: Vec<PathBuf>,

    /// List of dependencies that need to be up to date
    #[serde(deserialize_with = "deserialize_path_list")]
    #[serde(alias = "dep", alias = "dependency", alias = "requires")]
    #[serde(default)]
    dependencies: Vec<PathBuf>,

    /// List of commands needed to execute the rule. Can be empty if we just want to manipulate virtual targets
    #[serde(deserialize_with = "deserialize_task_list")]
    #[serde(alias = "cmd", alias = "command", alias = "launch", alias = "run")]
    #[serde(default)]
    commands: Vec<Task>,

    /// Potential help for the user
    help: Option<String>,

    /// Explicit for extern commands
    phony: Option<bool>,

    /// Constraint to disable the rule
    #[serde(deserialize_with = "deserialize_constraint")]
    #[serde(default)]
    constraint: Option<Constraint>
}

fn deserialize_task_list<'de, D>(deserializer: D) -> Result<Vec<Task>, D::Error>
where D: Deserializer<'de> {
    struct SequenceOrList;

    impl<'de> Visitor<'de> for SequenceOrList {
        type Value = Vec<Task>;

        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
            formatter.write_str("command or list of commands")
        }

        fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
        where A: serde::de::EnumAccess<'de> {
            let t: Task =
                Deserialize::deserialize(serde::de::value::EnumAccessDeserializer::new(data))?;
            Ok(vec![t])
        }

        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
        where E: serde::de::Error {
            let t: Task = Deserialize::deserialize(serde::de::value::StrDeserializer::new(v))?;
            Ok(vec![t])
        }

        fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
        where A: serde::de::SeqAccess<'de> {
            Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))
        }
    }

    deserializer.deserialize_any(SequenceOrList)
}

impl Rule {
    pub fn new_default<S: AsRef<str>>(targets: &[S], dependencies: &[S], kind: &str) -> Self {
        let task = match kind {
            "basm" => {
                Task::new_basm(&format!(
                    "-o {} {}",
                    targets[0].as_ref(),
                    dependencies[0].as_ref()
                ))
            },
            _ => unreachable!()
        };

        Self::new(targets, dependencies, &[task])
    }

    pub fn new<S: AsRef<str>, T: Into<Task> + Clone>(
        targets: &[S],
        dependencies: &[S],
        commands: &[T]
    ) -> Self {
        Self {
            targets: targets
                .into_iter()
                .map(|s| expand_glob(s.as_ref()))
                .flatten()
                .map(|s| PathBuf::from(s))
                .collect_vec(),
            dependencies: dependencies
                .into_iter()
                .map(|s| expand_glob(s.as_ref()))
                .flatten()
                .map(|s| PathBuf::from(s))
                .collect_vec(),
            commands: commands
                .into_iter()
                .map(|t| (t.clone()).into())
                .collect_vec(),
            help: None,
            phony: None,
            constraint: None
        }
    }

    pub fn commands(&self) -> &[Task] {
        &self.commands
    }

    /// Phony commands do not generate files and need to be launched each time (except in watch sessions)
    pub fn is_phony(&self) -> bool {
        if let Some(phony) = self.phony.as_ref() {
            return *phony;
        }

        if self.commands().is_empty() {
            true
        }
        else {
            self.commands().iter().all(|c| c.is_phony())
        }
    }

    pub fn is_up_to_date(&self) -> bool {
        // No need to try if one is missing
        if self.targets.iter().any(|p| !p.exists()) {
            return false;
        }

        let oldest_target = self
            .targets
            .iter()
            .filter(|p| p.exists())
            .map(|p| p.metadata().unwrap().modified().unwrap())
            .min();

        let newest_dependencies = self
            .dependencies
            .iter()
            .filter(|p| p.exists())
            .map(|p| p.metadata().unwrap().modified().unwrap())
            .max();

        oldest_target > newest_dependencies
    }

    pub fn help(&self) -> Option<&str> {
        self.help.as_ref().map(|s| s.as_str())
    }

    pub fn target(&self, idx: usize) -> &Path {
        &self.targets[idx]
    }

    pub fn targets(&self) -> &[PathBuf] {
        &self.targets
    }

    pub fn command(&self, idx: usize) -> &Task {
        &self.commands[idx]
    }

    pub fn dependencies(&self) -> &[PathBuf] {
        &self.dependencies
    }

    pub fn is_enabled(&self) -> bool {
        if let Some(constraint) = &self.constraint {
            constraint.corresponds()
        }
        else {
            true
        }
    }
}
