use super::*;
use crate::gradient::Gradient;
use chrono::DateTime;
use chrono::Utc;
use std::collections::BTreeMap;
use std::collections::HashSet;

impl Problem {
    /// Returns all possible transformations for the pattern. Not for the method.
    #[must_use]
    pub fn transformations(&self, wall_dimensions: WallDimensions) -> HashSet<Self> {
        self.pattern
            .transformations(wall_dimensions)
            .into_iter()
            .map(|pattern| Self {
                pattern,
                method: self.method,
            })
            .collect()
    }
}

impl Pattern {
    #[must_use]
    pub fn canonicalize(&self) -> Self {
        let mut pattern = self.clone();
        let min_col = pattern.pattern.iter().map(|(hold_position, _)| hold_position.col).min().unwrap_or(0);
        pattern.pattern = pattern
            .pattern
            .iter()
            .map(|(hold_position, hold_role)| {
                let hold_position = HoldPosition {
                    row: hold_position.row,
                    col: hold_position.col - min_col,
                };
                (hold_position, *hold_role)
            })
            .collect();
        std::cmp::min(pattern.mirror(), pattern)
    }

    #[must_use]
    pub fn shift_left(&self, shift: u64) -> Option<Self> {
        // Out of bounds check
        if let Some(min_col) = self.pattern.iter().map(|(hold_position, _)| hold_position.col).min() {
            if shift > min_col {
                return None;
            }
        }

        let pattern: BTreeMap<HoldPosition, HoldRole> = self
            .pattern
            .iter()
            .map(|(hold_position, hold_role)| {
                let mut hold_position = hold_position.clone();
                hold_position.col -= shift;
                (hold_position, *hold_role)
            })
            .collect();

        Some(Self { pattern })
    }

    #[must_use]
    pub fn shift_right(&self, wall_dimensions: WallDimensions, shift: u64) -> Option<Self> {
        // Out of bounds check
        if let Some(max_col) = self.pattern.iter().map(|(hold_position, _)| hold_position.col).max() {
            if max_col + shift >= wall_dimensions.cols {
                return None;
            }
        }

        let pattern: BTreeMap<HoldPosition, HoldRole> = self
            .pattern
            .iter()
            .map(|(hold_position, hold_role)| {
                let mut hold_position = hold_position.clone();
                hold_position.col += shift;
                (hold_position, *hold_role)
            })
            .collect();

        Some(Self { pattern })
    }

    #[must_use]
    pub fn mirror(&self) -> Self {
        let mut pattern = self.clone();

        let min_col = pattern.pattern.iter().map(|(hold_position, _)| hold_position.col).min().unwrap_or(0);
        let max_col = pattern.pattern.iter().map(|(hold_position, _)| hold_position.col).max().unwrap_or(0);

        pattern.pattern = pattern
            .pattern
            .iter()
            .map(|(hold_position, hold_role)| {
                let HoldPosition { row, col } = *hold_position;
                let mut mirrored_col = col;
                mirrored_col += 2 * (max_col - col);
                mirrored_col -= max_col - min_col;
                let hold_position = HoldPosition { row, col: mirrored_col };
                (hold_position, *hold_role)
            })
            .collect();

        pattern
    }

    /// Returns all possible transformations for the pattern
    #[must_use]
    pub fn transformations(&self, wall_dimensions: WallDimensions) -> HashSet<Self> {
        let mut transformations = HashSet::new();

        let pattern = self.canonicalize();
        for mut pat in [pattern.mirror(), pattern] {
            transformations.insert(pat.clone());
            while let Some(p) = pat.shift_right(wall_dimensions, 1) {
                transformations.insert(p.clone());
                pat = p;
            }
        }

        transformations
    }
}

impl UserInteraction {
    pub(crate) fn new(wall_uid: WallUid, problem: Problem) -> Self {
        Self {
            wall_uid,
            problem,
            is_favorite: false,
            attempted_on: BTreeMap::new(),
        }
    }

    pub(crate) fn latest_attempt(&self) -> Option<DatedAttempt> {
        self.attempted_on.last_key_value().map(Into::into)
    }

    pub(crate) fn todays_attempt(&self) -> Option<Attempt> {
        self.latest_attempt()
            .filter(|latest_attempt| {
                let today_local_naive = chrono::Local::now().date_naive();
                let datetime_local_naive = latest_attempt.date_time.with_timezone(&chrono::Local).date_naive();
                datetime_local_naive == today_local_naive
            })
            .map(|dated_attempt| dated_attempt.attempt)
    }

    pub(crate) fn best_attempt(&self) -> Option<DatedAttempt> {
        self.attempted_on.iter().max_by_key(|(_date_time, attempt)| *attempt).map(Into::into)
    }

    pub(crate) fn attempted_on(&self) -> impl IntoIterator<Item = DatedAttempt> {
        self.attempted_on.iter().rev().map(Into::into)
    }
}

impl From<(DateTime<Utc>, Attempt)> for DatedAttempt {
    fn from(value: (DateTime<Utc>, Attempt)) -> Self {
        let (date_time, attempt) = value;
        DatedAttempt { date_time, attempt }
    }
}

impl From<&(DateTime<Utc>, Attempt)> for DatedAttempt {
    fn from(value: &(DateTime<Utc>, Attempt)) -> Self {
        let &(date_time, attempt) = value;
        DatedAttempt { date_time, attempt }
    }
}

impl From<(&DateTime<Utc>, &Attempt)> for DatedAttempt {
    fn from(value: (&DateTime<Utc>, &Attempt)) -> Self {
        let (&date_time, &attempt) = value;
        DatedAttempt { date_time, attempt }
    }
}

impl WallUid {
    #[expect(dead_code)]
    pub(crate) fn create() -> Self {
        Self(uuid::Uuid::new_v4())
    }
    #[expect(dead_code)]
    pub(crate) fn min() -> Self {
        Self(uuid::Uuid::nil())
    }
    #[expect(dead_code)]
    pub(crate) fn max() -> Self {
        Self(uuid::Uuid::max())
    }
}

impl Image {
    pub(crate) fn srcset(&self) -> String {
        self.resolutions
            .iter()
            .map(|(res, filename)| format!("/files/holds/{} {}w", filename.filename, res.width))
            .collect::<Vec<String>>()
            .join(", ")
    }
}

impl ImageUid {
    pub(crate) fn create() -> Self {
        Self(uuid::Uuid::new_v4())
    }
}

impl Attempt {
    pub(crate) fn icon(&self) -> crate::components::icons::Icon {
        use crate::components::icons::Icon;
        match self {
            Attempt::Attempt => Icon::ArrowTrendingUp,
            Attempt::Send => Icon::Trophy,
            Attempt::Flash => Icon::BoltSolid,
        }
    }

    pub(crate) fn gradient(&self) -> Gradient {
        match self {
            Attempt::Attempt => Gradient::PinkOrange,
            Attempt::Send => Gradient::TealLime,
            Attempt::Flash => Gradient::CyanBlue,
        }
    }
}

impl std::str::FromStr for Problem {
    type Err = ron::Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let problem = ron::from_str(s)?;
        Ok(problem)
    }
}

impl std::fmt::Display for Problem {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = ron::to_string(self).unwrap();
        write!(f, "{s}")?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test_try::test_try]
    fn canonicalize_empty_pattern() {
        let pattern = Pattern {
            pattern: [].into_iter().collect(),
        };
        let canonicalized = pattern.canonicalize();
        assert_eq!(pattern, canonicalized);
        let mirrored = pattern.mirror();
        assert_eq!(pattern, mirrored);
    }

    #[test_try::test_try]
    fn canonicalize_pattern() {
        let pattern = Pattern {
            pattern: [
                (HoldPosition { row: 0, col: 1 }, HoldRole::End),
                (HoldPosition { row: 7, col: 6 }, HoldRole::Start),
            ]
            .into_iter()
            .collect(),
        };

        let canonicalized = pattern.canonicalize();

        assert_eq!(canonicalized.pattern[&HoldPosition { row: 0, col: 0 }], HoldRole::End);
        assert_eq!(canonicalized.pattern[&HoldPosition { row: 7, col: 5 }], HoldRole::Start);
    }

    #[test_try::test_try]
    fn mirror_pattern() {
        let pattern = Pattern {
            pattern: [
                (HoldPosition { row: 0, col: 1 }, HoldRole::End),
                (HoldPosition { row: 7, col: 6 }, HoldRole::Start),
            ]
            .into_iter()
            .collect(),
        };

        let mirrored = pattern.mirror();

        assert_eq!(mirrored.pattern[&HoldPosition { row: 0, col: 6 }], HoldRole::End);
        assert_eq!(mirrored.pattern[&HoldPosition { row: 7, col: 1 }], HoldRole::Start);
    }
}
