use std::fmt::{self, Write};

use crate::{
    progress::Step,
    unit::{DisplayValue, Unit},
};

/// The location at which [`Throughput`] or [`UnitDisplays`][UnitDisplay] should be placed.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
#[allow(missing_docs)]
pub enum Location {
    BeforeValue,
    AfterUnit,
}

/// A structure able to display throughput, a value change within a given duration.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
pub struct Throughput {
    /// The change of value between the current value and the previous one.
    pub value_change_in_timespan: Step,
    /// The amount of time passed between the previous and the current value.
    pub timespan: std::time::Duration,
}

impl Throughput {
    /// A convenience method to create a new ThroughPut from `value_change_in_timespan` and `timespan`.
    pub fn new(value_change_in_timespan: Step, timespan: std::time::Duration) -> Self {
        Throughput {
            value_change_in_timespan,
            timespan,
        }
    }
}

/// A way to display a [Unit].
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
pub struct Mode {
    location: Location,
    percent: bool,
    throughput: bool,
}

impl Mode {
    fn percent_location(&self) -> Option<Location> {
        if self.percent {
            Some(self.location)
        } else {
            None
        }
    }

    fn throughput_location(&self) -> Option<Location> {
        if self.throughput {
            Some(self.location)
        } else {
            None
        }
    }
}

/// initialization and modification
impl Mode {
    /// Create a mode instance with percentage only.
    pub fn with_percentage() -> Self {
        Mode {
            percent: true,
            throughput: false,
            location: Location::AfterUnit,
        }
    }
    /// Create a mode instance with throughput only.
    pub fn with_throughput() -> Self {
        Mode {
            percent: false,
            throughput: true,
            location: Location::AfterUnit,
        }
    }
    /// Turn on percentage display on the current instance.
    pub fn and_percentage(mut self) -> Self {
        self.percent = true;
        self
    }
    /// Turn on throughput display on the current instance.
    pub fn and_throughput(mut self) -> Self {
        self.throughput = true;
        self
    }
    /// Change the display location to show up in front of the value.
    pub fn show_before_value(mut self) -> Self {
        self.location = Location::BeforeValue;
        self
    }
}

/// A utility to implement [Display][std::fmt::Display].
pub struct UnitDisplay<'a> {
    pub(crate) current_value: Step,
    pub(crate) upper_bound: Option<Step>,
    pub(crate) throughput: Option<Throughput>,
    pub(crate) parent: &'a Unit,
    pub(crate) display: What,
}

pub(crate) enum What {
    ValuesAndUnit,
    Unit,
    Values,
}

impl What {
    fn values(&self) -> bool {
        matches!(self, What::Values | What::ValuesAndUnit)
    }
    fn unit(&self) -> bool {
        matches!(self, What::Unit | What::ValuesAndUnit)
    }
}

impl UnitDisplay<'_> {
    /// Display everything, values and the unit.
    pub fn all(&mut self) -> &Self {
        self.display = What::ValuesAndUnit;
        self
    }
    /// Display only values.
    pub fn values(&mut self) -> &Self {
        self.display = What::Values;
        self
    }
    /// Display only units.
    pub fn unit(&mut self) -> &Self {
        self.display = What::Unit;
        self
    }
}

impl fmt::Display for UnitDisplay<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let unit: &dyn DisplayValue = self.parent.as_display_value();
        let mode = self.parent.mode;

        let percent_location_and_fraction = self.upper_bound.and_then(|upper| {
            mode.and_then(|m| m.percent_location())
                .map(|location| (location, ((self.current_value as f64 / upper as f64) * 100.0).floor()))
        });
        let throughput_and_location = self.throughput.as_ref().and_then(|throughput| {
            mode.and_then(|m| m.throughput_location())
                .map(|location| (location, throughput))
        });
        if self.display.values() {
            if let Some((Location::BeforeValue, fraction)) = percent_location_and_fraction {
                unit.display_percentage(f, fraction)?;
                f.write_char(' ')?;
            }
            if let Some((Location::BeforeValue, throughput)) = throughput_and_location {
                unit.display_throughput(f, throughput)?;
                f.write_char(' ')?;
            }
            unit.display_current_value(f, self.current_value, self.upper_bound)?;
            if let Some(upper) = self.upper_bound {
                unit.separator(f, self.current_value, self.upper_bound)?;
                unit.display_upper_bound(f, upper, self.current_value)?;
            }
        }
        if self.display.unit() {
            let mut buf = String::with_capacity(10);
            if self.display.values() {
                buf.write_char(' ')?;
            }
            unit.display_unit(&mut buf, self.current_value)?;
            if buf.len() > 1 {
                // did they actually write a unit?
                f.write_str(&buf)?;
            }

            if let Some((Location::AfterUnit, fraction)) = percent_location_and_fraction {
                f.write_char(' ')?;
                unit.display_percentage(f, fraction)?;
            }
            if let Some((Location::AfterUnit, throughput)) = throughput_and_location {
                f.write_char(' ')?;
                unit.display_throughput(f, throughput)?;
            }
        }
        Ok(())
    }
}
