use std::convert::TryFrom;
use std::fmt;
use std::mem;
use std::ops::{Deref, Sub};

use colored::Colorize;
use colorgrad::{Gradient, GradientBuilder};
use jiff::{Timestamp, Unit, tz::TimeZone};

use crate::temporal::ZonedExt;
use crate::util;

// Taken straight from d3: https://github.com/d3/d3-array/blob/main/src/ticks.js
const E10: f64 = 7.0710678118654755; // sqrt(50)
const E5: f64 = 3.1622776601683795; // sqrt(10)
const E2: f64 = std::f64::consts::SQRT_2; // sqrt(2)

fn tick_spec(start: f64, stop: f64, count: f64) -> (f64, f64, f64) {
    let step = (stop - start) / count.max(0.0);
    let power = step.log10().floor();
    let error = step / 10.0_f64.powf(power);
    let factor = if error >= E10 {
        10.0
    } else if error >= E5 {
        5.0
    } else if error >= E2 {
        2.0
    } else {
        1.0
    };

    let (i1, i2, inc) = if power < 0.0 {
        let mut inc = 10.0_f64.powf(-power) / factor;
        let mut i1 = (start * inc).round();
        let mut i2 = (stop * inc).round();
        if i1 / inc < start {
            i1 += 1.0;
        }
        if i2 / inc > stop {
            i2 -= 1.0;
        }
        inc = -inc;

        (i1, i2, inc)
    } else {
        let inc = 10.0_f64.powf(power) * factor;
        let mut i1 = (start / inc).round();
        let mut i2 = (stop / inc).round();
        if i1 * inc < start {
            i1 += 1.0;
        }
        if i2 * inc > stop {
            i2 -= 1.0;
        }

        (i1, i2, inc)
    };

    if i2 < i1 && (0.5..2.0).contains(&count) {
        return tick_spec(start, stop, count * 2.0);
    }

    (i1, i2, inc)
}

fn tick_increment(start: f64, stop: f64, count: f64) -> f64 {
    tick_spec(start, stop, count).2
}

fn ticks(start: f64, stop: f64, count: f64) -> Vec<f64> {
    if count == 0.0 {
        return vec![];
    }

    if start == stop {
        return vec![start];
    }

    let reverse = stop < start;

    let (i1, i2, inc) = if reverse {
        tick_spec(stop, start, count)
    } else {
        tick_spec(start, stop, count)
    };

    if i2 < i1 {
        return vec![];
    }

    let n = (i2 - i1 + 1.0) as usize;

    let mut ticks = Vec::with_capacity(n);

    if reverse {
        if inc < 0.0 {
            for i in 0..n {
                ticks.push((i2 - i as f64) / -inc);
            }
        } else {
            for i in 0..n {
                ticks.push((i2 - i as f64) * inc);
            }
        }
    } else if inc < 0.0 {
        for i in 0..n {
            ticks.push((i1 + i as f64) / -inc);
        }
    } else {
        for i in 0..n {
            ticks.push((i1 + i as f64) * inc);
        }
    }

    ticks
}

fn linear_nice(domain: (f64, f64), count: usize) -> Option<(f64, f64)> {
    let (mut start, mut stop) = domain;

    let mut step: f64;
    let mut previous_step: Option<f64> = None;

    if stop < start {
        mem::swap(&mut start, &mut stop);
    }

    for _ in 0..10 {
        step = tick_increment(start, stop, count as f64);

        if matches!(previous_step, Some(s) if s == step) {
            return Some(if stop < start {
                (stop, start)
            } else {
                (start, stop)
            });
        }

        if step > 0.0 {
            start = (start / step).floor() * step;
            stop = (stop / step).ceil() * step;
        } else if step < 0.0 {
            start = (start * step).ceil() / step;
            stop = (stop * step).floor() / step;
        } else {
            break;
        }

        previous_step = Some(step);
    }

    None
}

#[inline]
fn lerp(min: f64, max: f64, t: f64) -> f64 {
    (1.0 - t) * min + t * max
}

#[derive(Debug, Clone, Copy)]
pub struct Extent<T>((T, T));

impl<T: Copy + PartialOrd> Extent<T> {
    #[inline]
    fn constant(value: T) -> Self {
        Self((value, value))
    }

    #[inline]
    pub fn is_constant(&self) -> bool {
        self.0.0 == self.0.1
    }

    #[inline]
    pub fn set_min(&mut self, value: T) {
        self.0.0 = value;
    }

    #[inline]
    pub fn set_max(&mut self, value: T) {
        self.0.1 = value;
    }

    #[inline]
    pub fn min(&self) -> T {
        self.0.0
    }

    #[inline]
    pub fn max(&self) -> T {
        self.0.1
    }

    pub fn into_inner(self) -> (T, T) {
        self.0
    }

    fn process(&mut self, value: T) {
        if value < self.0.0 {
            self.0.0 = value;
        }

        if value > self.0.1 {
            self.0.1 = value;
        }
    }
}

impl<T: Copy + PartialOrd + Sub<Output = T>> Extent<T> {
    #[inline]
    pub fn width(&self) -> T {
        self.max() - self.min()
    }
}

impl Extent<f64> {
    pub fn discrete_index(&self, bins: usize, target: f64) -> usize {
        let bin_width = self.width() / bins as f64;
        let index = ((target - self.min()) / bin_width).floor() as usize;

        index.min(bins.saturating_sub(1))
    }

    #[inline]
    fn lerp(&self, t: f64) -> f64 {
        lerp(self.min(), self.max(), t)
    }
}

impl<T: Copy + PartialOrd> From<(T, T)> for Extent<T> {
    fn from(value: (T, T)) -> Self {
        Self(value)
    }
}

// NOTE: this builder is also able to clamp values when processing them.
#[derive(Debug, Clone)]
pub struct ExtentBuilder<T> {
    extent: Option<Extent<T>>,
    min_clamp: Option<T>,
    max_clamp: Option<T>,
}

impl<T: Copy + PartialOrd> ExtentBuilder<T> {
    pub fn new() -> Self {
        Self {
            extent: None,
            max_clamp: None,
            min_clamp: None,
        }
    }

    pub fn clear(&mut self) {
        self.extent = None;
        self.min_clamp = None;
        self.max_clamp = None;
    }

    pub fn clamp_min(&mut self, min: T) {
        self.min_clamp = Some(min);
    }

    pub fn clamp_max(&mut self, max: T) {
        self.max_clamp = Some(max);
    }

    pub fn process(&mut self, value: T) -> bool {
        if let Some(min) = self.min_clamp.as_ref() {
            if value < *min {
                return false;
            }
        }

        if let Some(max) = self.max_clamp.as_ref() {
            if value > *max {
                return false;
            }
        }

        match self.extent.as_mut() {
            None => {
                self.extent = Some(Extent::constant(value));
            }
            Some(extent) => extent.process(value),
        };

        true
    }

    pub fn build(&self) -> Option<Extent<T>> {
        if let (Some(min), Some(max)) = (self.min_clamp, self.max_clamp) {
            return Some(Extent::from((min, max)));
        }

        self.extent.map(|mut extent| {
            if let Some(min_clamp) = self.min_clamp {
                extent.set_min(min_clamp);
            }
            if let Some(max_clamp) = self.max_clamp {
                extent.set_max(max_clamp);
            }

            extent
        })
    }

    pub fn merge(&mut self, other: &Self) {
        if let Some(extent) = other.extent.as_ref() {
            self.process(extent.min());
            self.process(extent.max());
        }
    }
}

impl<T: Copy + PartialOrd> From<(Option<T>, Option<T>)> for ExtentBuilder<T> {
    fn from(value: (Option<T>, Option<T>)) -> Self {
        let mut extent_builder = Self::new();

        if let Some(min) = value.0 {
            extent_builder.clamp_min(min);
        }

        if let Some(max) = value.1 {
            extent_builder.clamp_max(max);
        }

        extent_builder
    }
}

pub fn sturges(len: usize) -> usize {
    1 + (len as f64).log2().ceil() as usize
}

pub struct HistogramBuilder {
    bins: Vec<f64>,
    domain_extent: Extent<f64>,
    bin_width: f64,
    max_count: f64,
    max_value: Option<f64>,
}

impl HistogramBuilder {
    pub fn new(bins: usize, extent: Extent<f64>) -> Self {
        Self {
            bins: vec![0.0; bins],
            domain_extent: extent,
            bin_width: extent.width() / bins as f64,
            max_count: 0.0,
            max_value: None,
        }
    }

    #[inline(always)]
    pub fn bins(&self) -> usize {
        self.bins.len()
    }

    pub fn max_count(&self) -> u64 {
        self.max_count as u64
    }

    pub fn max_value(&self) -> f64 {
        self.max_value.unwrap_or(self.max_count)
    }

    pub fn should_use_log_scale(&self) -> bool {
        self.domain_extent.max() / self.domain_extent.min() > 1_000.0
    }

    // pub fn should_shrink(&self) -> bool {
    //     let empty = self.bins.iter().map(|b| *b == 0.0).count();

    //     empty >= self.bins() / 2
    // }

    pub fn discrete_index(&self, x: f64) -> usize {
        (((x - self.domain_extent.min()) / self.bin_width).floor() as usize).min(self.bins() - 1)
    }

    #[inline(always)]
    pub fn add(&mut self, x: f64) {
        self.add_n(x, 1);
    }

    pub fn add_n(&mut self, x: f64, n: usize) {
        let index = self.discrete_index(x);

        let count = self.bins[index] + n as f64;

        if count > self.max_count {
            self.max_count = count;
        }

        self.bins[index] = count;
    }

    pub fn from_series(bins: usize, extent: Extent<f64>, series: &[f64]) -> Self {
        let mut histogram = Self::new(bins, extent);

        for x in series.iter().copied() {
            histogram.add(x);
        }

        histogram
    }

    pub fn into_vec(self) -> Vec<f64> {
        self.bins
    }
}

impl Deref for HistogramBuilder {
    type Target = [f64];

    fn deref(&self) -> &Self::Target {
        &self.bins
    }
}

macro_rules! build_gradient_name {
    ($(($enum_name: ident, $name: ident, $string: literal),)*) => {

        #[derive(Clone, Copy, Deserialize, Debug)]
        #[serde(try_from = "String")]
        pub enum GradientName {
            Siren,
            $(
                $enum_name,
            )*
        }

        impl GradientName {
            pub fn as_str(&self) -> &str {
                match self {
                    Self::Siren => "siren",
                    $(
                        Self::$enum_name => $string,
                    )*
                }
            }

            pub fn build(&self) -> Box<dyn Gradient> {
                match self {
                    Self::Siren => {
                        Box::new(GradientBuilder::new()
                            .html_colors(&["#FF1493", "#ffd700", "#2e8b57"])
                            .build::<colorgrad::CatmullRomGradient>().unwrap())
                    },
                    $(
                        Self::$enum_name => Box::new(colorgrad::preset::$name()),
                    )*
                }
            }
        }

        impl TryFrom<String> for GradientName {
            type Error = String;

            fn try_from(value: String) -> Result<Self, Self::Error> {
                Ok(match value.as_str() {
                    "siren" => Self::Siren,
                    $(
                        $string => Self::$enum_name,
                    )*
                    _ => return Err(format!("unknown gradient \"{}\"", &value)),
                })
            }
        }
    };
}

build_gradient_name! {
    // Sequential (Single-Hue)
    (Blues, blues, "blues"),
    (Greens, greens, "greens"),
    (Greys, greys, "greys"),
    (Oranges, oranges, "oranges"),
    (Purples, purples, "purples"),
    (Reds, reds, "reds"),

    // Sequential (Multi-Hue)
    (Turbo, turbo, "turbo"),
    (Viridis, viridis, "viridis"),
    (Inferno, inferno, "inferno"),
    (Magma, magma, "magma"),
    (Plasma, plasma, "plasma"),
    (Cividis, cividis, "cividis"),
    (Warm, warm, "warm"),
    (Cool, cool, "cool"),
    (CubehelixDefault, cubehelix_default, "cubehelix_default"),
    (BuGn, bu_gn, "bu_gn"),
    (BuPu, bu_pu, "bu_pu"),
    (GnBu, gn_bu, "gn_bu"),
    (OrRd, or_rd, "or_rd"),
    (PuBuGn, pu_bu_gn, "pu_bu_gn"),
    (PuBu, pu_bu, "pu_bu"),
    (PuRd, pu_rd, "pu_rd"),
    (RdPu, rd_pu, "rd_pu"),
    (YlGnBu, yl_gn_bu, "yl_gn_bu"),
    (YlGn, yl_gn, "yl_gn"),
    (YlOrBr, yl_or_br, "yl_or_br"),
    (YlOrRd, yl_or_rd, "yl_or_rd"),

    // Diverging
    (BrBg, br_bg, "br_bg"),
    (PrGn, pr_gn, "pr_gn"),
    (PiYg, pi_yg, "pi_yg"),
    (PuOr, pu_or, "pu_or"),
    (RdBu, rd_bu, "rd_bu"),
    (RdGy, rd_gy, "rd_gy"),
    (RdYlBu, rd_yl_bu, "rd_yl_bu"),
    (RdYlGn, rd_yl_gn, "rd_yl_gn"),
    (Spectral, spectral, "spectral"),

    // Cyclical
    (Rainbow, rainbow, "rainbow"),
    (Sinebow, sinebow, "sinebow"),
}

impl GradientName {
    pub fn sample(&self) -> String {
        self.build()
            .colors(30)
            .into_iter()
            .map(|c| {
                let rgb = c.to_rgba8();
                "  ".on_truecolor(rgb[0], rgb[1], rgb[2]).to_string()
            })
            .collect::<Vec<_>>()
            .join("")
    }

    pub fn single_hue_sequential_iter() -> impl Iterator<Item = Self> {
        use GradientName::*;
        [Blues, Greens, Greys, Oranges, Purples, Reds]
            .iter()
            .copied()
    }

    pub fn multi_hue_sequential_iter() -> impl Iterator<Item = Self> {
        use GradientName::*;
        [
            Turbo,
            Viridis,
            Inferno,
            Magma,
            Plasma,
            Cividis,
            Siren,
            Warm,
            Cool,
            CubehelixDefault,
            BuGn,
            BuPu,
            GnBu,
            OrRd,
            PuBuGn,
            PuBu,
            PuRd,
            RdPu,
            YlGnBu,
            YlGn,
            YlOrBr,
            YlOrRd,
        ]
        .iter()
        .copied()
    }

    pub fn diverging_iter() -> impl Iterator<Item = Self> {
        use GradientName::*;
        [BrBg, PrGn, PiYg, PuOr, RdBu, RdGy, RdYlBu, RdYlGn, Spectral]
            .iter()
            .copied()
    }

    pub fn cyclical_iter() -> impl Iterator<Item = Self> {
        use GradientName::*;
        [Rainbow, Sinebow].iter().copied()
    }
}

#[derive(Debug, Clone, Copy)]
pub enum PowerExponent {
    Two,
    Int(i32),
    Float(f64),
    Sqrt,
}

impl PowerExponent {
    #[inline(always)]
    fn apply(&self, x: f64) -> f64 {
        match self {
            Self::Two => x * x,
            Self::Int(i) => x.powi(*i),
            Self::Float(f) => x.powf(*f),
            Self::Sqrt => x.sqrt(),
        }
    }

    // #[inline(always)]
    // fn invert(&self, x: f64) -> f64 {
    //     match self {
    //         Self::Two => x.sqrt(),
    //         Self::Int(i) => x.powf(1.0 / *i as f64),
    //         Self::Float(f) => x.powf(1.0 / *f),
    //         Self::Sqrt => x * x,
    //     }
    // }
}

#[derive(Debug, Clone, Copy)]
pub enum LogBase {
    Natural,
    Base2,
    Base10,
    Ln1p,
    Custom(f64),
}

impl LogBase {
    #[inline(always)]
    fn is_integer(&self) -> bool {
        match self {
            Self::Natural => false,
            Self::Base2 => true,
            Self::Base10 => true,
            Self::Ln1p => false,
            Self::Custom(base) => base.fract() <= f64::EPSILON,
        }
    }

    #[inline(always)]
    fn as_float(&self) -> f64 {
        match self {
            Self::Natural => std::f64::consts::E,
            Self::Base2 => 2.0,
            Self::Base10 => 10.0,
            Self::Ln1p => std::f64::consts::E,
            Self::Custom(base) => *base,
        }
    }

    #[inline(always)]
    fn apply(&self, x: f64) -> f64 {
        match self {
            Self::Natural => x.ln(),
            Self::Base2 => x.log2(),
            Self::Base10 => x.log10(),
            Self::Ln1p => x.ln_1p(),
            Self::Custom(base) => x.log(*base),
        }
    }

    #[inline(always)]
    fn invert(&self, x: f64) -> f64 {
        match self {
            Self::Natural => x.exp(),
            Self::Base2 => x.exp2(),
            Self::Base10 => 10.0_f64.powf(x),
            Self::Ln1p => x.exp_m1(),
            Self::Custom(base) => base.powf(x),
        }
    }

    #[inline(always)]
    fn accepts_zero(&self) -> bool {
        matches!(self, Self::Ln1p)
    }
}

#[derive(Debug, Clone, Copy, Default, Deserialize)]
#[serde(try_from = "String")]
pub enum ScaleType {
    #[default]
    Linear,
    Logarithmic(LogBase),
    Power(PowerExponent),
}

impl ScaleType {
    pub fn ln() -> Self {
        Self::Logarithmic(LogBase::Natural)
    }

    pub fn is_linear(&self) -> bool {
        matches!(self, Self::Linear)
    }

    #[inline]
    pub fn disallows_zero(&self) -> bool {
        matches!(self, Self::Logarithmic(base) if !base.accepts_zero())
    }

    #[inline]
    pub fn accepts(&self, x: f64) -> bool {
        match self {
            Self::Linear | Self::Power(_) => true,
            Self::Logarithmic(base) => {
                if base.accepts_zero() {
                    x >= 0.0
                } else {
                    x > 0.0
                }
            }
        }
    }

    #[inline]
    pub fn is_logarithmic(&self) -> bool {
        matches!(self, Self::Logarithmic(_))
    }
}

impl TryFrom<String> for ScaleType {
    type Error = String;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Ok(match value.as_str() {
            "lin" => Self::Linear,
            "log" | "ln" => Self::Logarithmic(LogBase::Natural),
            "log2" => Self::Logarithmic(LogBase::Base2),
            "log10" => Self::Logarithmic(LogBase::Base10),
            "ln_1p" | "ln1p" => Self::Logarithmic(LogBase::Ln1p),
            v if v.starts_with("log(") && v.ends_with(")") => {
                let base_str = v.split("log(").nth(1).unwrap().trim_end_matches(')');

                if let Ok(base) = base_str.parse::<f64>() {
                    Self::Logarithmic(LogBase::Custom(base))
                } else {
                    return Err(format!("could not parse log base \"{}\"", base_str));
                }
            }
            "pow" => Self::Power(PowerExponent::Two),
            "sqrt" => Self::Power(PowerExponent::Sqrt),
            v if v.starts_with("pow(") && v.ends_with(")") => {
                let exponent_str = v.split("pow(").nth(1).unwrap().trim_end_matches(')');

                if let Ok(exponent) = exponent_str.parse::<i32>() {
                    Self::Power(PowerExponent::Int(exponent))
                } else if let Ok(exponent) = exponent_str.parse::<f64>() {
                    Self::Power(PowerExponent::Float(exponent))
                } else {
                    return Err(format!("could not parse pow exponent \"{}\"", exponent_str));
                }
            }
            _ => return Err(format!("unknown scale type \"{}\"", &value)),
        })
    }
}

impl fmt::Display for ScaleType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Linear => write!(f, "linear"),
            Self::Logarithmic(base) => match base {
                LogBase::Base2 => write!(f, "log2"),
                LogBase::Base10 => write!(f, "log10"),
                LogBase::Natural => write!(f, "log"),
                LogBase::Ln1p => write!(f, "ln_1p"),
                LogBase::Custom(base) => write!(f, "log({})", base),
            },
            Self::Power(exponent) => match exponent {
                PowerExponent::Two => write!(f, "pow"),
                PowerExponent::Int(i) => write!(f, "pow({})", i),
                PowerExponent::Float(e) => write!(f, "pow({})", e),
                PowerExponent::Sqrt => write!(f, "sqrt"),
            },
        }
    }
}

#[derive(Debug, Clone)]
pub struct LinearScale {
    input_domain: Extent<f64>,
    output_range: Extent<f64>,
}

impl LinearScale {
    pub fn new(input_domain: (f64, f64), output_range: (f64, f64)) -> Self {
        assert!(input_domain.0 <= input_domain.1, "input_domain min > max");
        assert!(output_range.0 <= output_range.1, "output_range min > max");

        Self {
            input_domain: Extent::from(input_domain),
            output_range: Extent::from(output_range),
        }
    }

    pub fn from_extent(input_domain: Extent<f64>) -> Self {
        Self::new(input_domain.into_inner(), (0.0, 1.0))
    }

    pub fn nice(input_domain: (f64, f64), output_range: (f64, f64), ticks: usize) -> Self {
        Self::new(
            linear_nice(input_domain, ticks).unwrap_or(input_domain),
            output_range,
        )
    }

    #[inline]
    pub fn ratio(&self, value: f64) -> f64 {
        (value - self.input_domain.min()) / self.input_domain.width()
    }

    pub fn map(&self, value: f64) -> f64 {
        let ratio = self.ratio(value);

        ratio * self.output_range.width() + self.output_range.min()
    }

    pub fn ticks(&self, count: usize) -> Vec<f64> {
        ticks(
            self.input_domain.min(),
            self.input_domain.max(),
            count as f64,
        )
    }

    fn formatted_ticks(&self, count: usize) -> Vec<String> {
        self.ticks(count)
            .into_iter()
            .map(util::format_number)
            .collect()
    }
}

fn format_timestamp(microseconds: i64, unit: Unit, timezone: TimeZone) -> String {
    // NOTE: I don't think rounding is necessary here because the formatting
    // will naturally hide the irrelevant part, right?
    // I will keep it, I am a superstitious.
    let zoned = Timestamp::from_microsecond(microseconds)
        .unwrap()
        .to_zoned(timezone)
        .floor(unit)
        .unwrap();

    zoned.to_string_wrt_unit(unit)
}

#[derive(Debug, Clone)]
pub struct TimeScale {
    input_domain: Extent<f64>,
    output_range: Extent<f64>,
    unit: Unit,
    timezone: TimeZone,
}

impl TimeScale {
    fn new(
        input_domain: (f64, f64),
        output_range: (f64, f64),
        unit: Unit,
        timezone: TimeZone,
    ) -> Self {
        assert!(input_domain.0 <= input_domain.1, "input_domain min > max");
        assert!(output_range.0 <= output_range.1, "output_range min > max");

        Self {
            input_domain: Extent::from(input_domain),
            output_range: Extent::from(output_range),
            unit,
            timezone,
        }
    }

    fn nice(
        input_domain: (f64, f64),
        output_range: (f64, f64),
        unit: Unit,
        timezone: TimeZone,
    ) -> Self {
        Self::new(input_domain, output_range, unit, timezone)
    }

    #[inline]
    fn ratio(&self, value: f64) -> f64 {
        (value - self.input_domain.min()) / self.input_domain.width()
    }

    fn map(&self, value: f64) -> f64 {
        let ratio = self.ratio(value);

        ratio * self.output_range.width() + self.output_range.min()
    }

    fn ticks(&self, count: usize) -> Vec<f64> {
        if count < 1 {
            return vec![];
        }

        if count < 3 {
            return vec![self.input_domain.min(), self.input_domain.max()];
        }

        let mut ticks = Vec::with_capacity(count);
        let mut t = 0.0;
        let fract = 1.0 / (count - 1) as f64;

        ticks.push(self.input_domain.min());

        for _ in 1..(count - 1) {
            t += fract;
            ticks.push(self.input_domain.lerp(t));
        }

        ticks.push(self.input_domain.max());

        ticks
    }

    fn formatted_ticks(&self, count: usize) -> Vec<String> {
        self.ticks(count)
            .into_iter()
            .map(|tick| format_timestamp(tick as i64, self.unit, self.timezone.clone()))
            .collect()
    }
}

#[derive(Debug, Clone)]
pub struct LogScale {
    base: LogBase,
    input_domain: Extent<f64>,
    converted_input_domain: Extent<f64>,
    output_range: Extent<f64>,
}

impl LogScale {
    fn new(base: LogBase, input_domain: (f64, f64), output_range: (f64, f64)) -> Self {
        assert!(input_domain.0 <= input_domain.1, "input_domain min > max");
        assert!(output_range.0 <= output_range.1, "output_range min > max");

        Self {
            base,
            input_domain: Extent::from(input_domain),
            converted_input_domain: Extent::from((
                base.apply(input_domain.0),
                base.apply(input_domain.1),
            )),
            output_range: Extent::from(output_range),
        }
    }

    fn nice(base: LogBase, input_domain: (f64, f64), output_range: (f64, f64)) -> Self {
        let input_domain = (
            base.invert(base.apply(input_domain.0).floor()),
            base.invert(base.apply(input_domain.1).ceil()),
        );

        Self::new(base, input_domain, output_range)
    }

    #[inline]
    fn ratio(&self, value: f64) -> f64 {
        (self.base.apply(value) - self.converted_input_domain.min())
            / self.converted_input_domain.width()
    }

    #[inline]
    fn map(&self, value: f64) -> f64 {
        let ratio = self.ratio(value);

        ratio * self.output_range.width() + self.output_range.min()
    }

    // NOTE: I do not support reverse scales (i.e. pow scales?)
    fn ticks(&self, count: usize) -> Vec<f64> {
        let u = self.input_domain.min();
        let v = self.input_domain.max();

        let mut i = self.converted_input_domain.min();
        let mut j = self.converted_input_domain.max();

        if !self.base.is_integer() && j - i < count as f64 {
            i = i.floor();
            j = j.ceil();

            let mut alt_ticks = Vec::new();

            if u > 0.0 {
                while i <= j {
                    let mut k = 1.0;

                    while k < self.base.as_float() {
                        let t = if i < 0.0 {
                            k / self.base.invert(-i)
                        } else {
                            k * self.base.invert(i)
                        };

                        if t < u {
                            continue;
                        }

                        if t > v {
                            break;
                        }

                        alt_ticks.push(t);

                        k += 1.0;
                    }

                    i += 1.0;
                }
            } else {
                while i <= j {
                    let mut k = self.base.as_float() - 1.0;

                    while k >= 1.0 {
                        let t = if i > 0.0 {
                            k / self.base.invert(-i)
                        } else {
                            k * self.base.invert(i)
                        };

                        if t < u {
                            continue;
                        }

                        if t > v {
                            break;
                        }

                        alt_ticks.push(t);

                        k -= 1.0;
                    }

                    i += 1.0;
                }
            }

            if alt_ticks.len() * 2 < count {
                ticks(u, v, count as f64)
            } else {
                alt_ticks
            }
        } else {
            ticks(i, j, (j - i).min(count as f64))
                .into_iter()
                .map(|tick| self.base.invert(tick))
                .collect()
        }
    }

    fn formatted_ticks(&self, count: usize) -> Vec<String> {
        self.ticks(count)
            .into_iter()
            .map(util::format_number)
            .collect()
    }
}

#[derive(Debug, Clone)]
pub struct PowerScale {
    exponent: PowerExponent,
    input_domain: Extent<f64>,
    converted_input_domain: Extent<f64>,
    output_range: Extent<f64>,
}

impl PowerScale {
    fn new(exponent: PowerExponent, input_domain: (f64, f64), output_range: (f64, f64)) -> Self {
        assert!(input_domain.0 <= input_domain.1, "input_domain min > max");
        assert!(output_range.0 <= output_range.1, "output_range min > max");

        Self {
            exponent,
            input_domain: Extent::from(input_domain),
            converted_input_domain: Extent::from((
                exponent.apply(input_domain.0),
                exponent.apply(input_domain.1),
            )),
            output_range: Extent::from(output_range),
        }
    }

    fn nice(
        exponent: PowerExponent,
        input_domain: (f64, f64),
        output_range: (f64, f64),
        ticks: usize,
    ) -> Self {
        Self::new(
            exponent,
            linear_nice(input_domain, ticks).unwrap_or(input_domain),
            output_range,
        )
    }

    #[inline]
    fn ratio(&self, value: f64) -> f64 {
        (self.exponent.apply(value) - self.converted_input_domain.min())
            / self.converted_input_domain.width()
    }

    #[inline]
    fn map(&self, value: f64) -> f64 {
        let ratio = self.ratio(value);

        ratio * self.output_range.width() + self.output_range.min()
    }

    pub fn ticks(&self, count: usize) -> Vec<f64> {
        ticks(
            self.input_domain.min(),
            self.input_domain.max(),
            count as f64,
        )
    }

    fn formatted_ticks(&self, count: usize) -> Vec<String> {
        self.ticks(count)
            .into_iter()
            .map(util::format_number)
            .collect()
    }
}

#[derive(Debug, Clone)]
pub enum Scale {
    Linear(LinearScale),
    Log(LogScale),
    Power(PowerScale),
    Time(TimeScale),
}

impl Scale {
    pub fn new(scale_type: ScaleType, input_domain: (f64, f64), output_range: (f64, f64)) -> Self {
        match scale_type {
            ScaleType::Linear => Self::Linear(LinearScale::new(input_domain, output_range)),
            ScaleType::Logarithmic(base) => {
                Self::Log(LogScale::new(base, input_domain, output_range))
            }
            ScaleType::Power(exponent) => {
                Self::Power(PowerScale::new(exponent, input_domain, output_range))
            }
        }
    }

    pub fn from_extent(scale_type: ScaleType, input_range: Extent<f64>) -> Self {
        Self::new(scale_type, input_range.into_inner(), (0.0, 1.0))
    }

    pub fn nice(
        scale_type: ScaleType,
        input_domain: (f64, f64),
        output_range: (f64, f64),
        ticks: usize,
    ) -> Self {
        match scale_type {
            ScaleType::Linear => Self::Linear(LinearScale::nice(input_domain, output_range, ticks)),
            ScaleType::Logarithmic(base) => {
                Self::Log(LogScale::nice(base, input_domain, output_range))
            }
            ScaleType::Power(exponent) => Self::Power(PowerScale::nice(
                exponent,
                input_domain,
                output_range,
                ticks,
            )),
        }
    }

    pub fn time(
        input_domain: (f64, f64),
        output_range: (f64, f64),
        unit: Unit,
        timezone: TimeZone,
    ) -> Self {
        Self::Time(TimeScale::nice(input_domain, output_range, unit, timezone))
    }

    pub fn is_logarithmic(&self) -> bool {
        matches!(self, Self::Log(_))
    }

    pub fn formatted_ticks(&self, count: usize) -> Vec<String> {
        match self {
            Self::Linear(inner) => inner.formatted_ticks(count),
            Self::Log(inner) => inner.formatted_ticks(count),
            Self::Power(inner) => inner.formatted_ticks(count),
            Self::Time(inner) => inner.formatted_ticks(count),
        }
    }

    pub fn ratio(&self, value: f64) -> f64 {
        match self {
            Self::Linear(inner) => inner.ratio(value),
            Self::Log(inner) => inner.ratio(value),
            Self::Power(inner) => inner.ratio(value),
            Self::Time(inner) => inner.ratio(value),
        }
    }

    pub fn map(&self, value: f64) -> f64 {
        match self {
            Self::Linear(inner) => inner.map(value),
            Self::Log(inner) => inner.map(value),
            Self::Power(inner) => inner.map(value),
            Self::Time(inner) => inner.map(value),
        }
    }
}

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

    #[test]
    fn test_ticks() {
        assert_eq!(ticks(0.0, 10.0, 0.0), Vec::<f64>::new());
        assert_eq!(ticks(1.0, 1.0, 10.0), vec![1.0]);
        assert_eq!(ticks(0.0, 10.0, 1.0), vec![0.0, 10.0]);
        assert_eq!(ticks(0.0, 10.0, 2.0), vec![0.0, 5.0, 10.0]);
        assert_eq!(ticks(0.0, 10.0, 3.0), vec![0.0, 5.0, 10.0]);
        assert_eq!(ticks(0.0, 10.0, 4.0), vec![0.0, 2.0, 4.0, 6.0, 8.0, 10.0]);
        assert_eq!(ticks(0.0, 10.0, 5.0), [0.0, 2.0, 4.0, 6.0, 8.0, 10.0]);
        assert_eq!(
            ticks(0.0, 10.0, 10.0),
            [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
        );
        assert_eq!(ticks(-5.0, 5.0, 3.0), vec![-5.0, 0.0, 5.0]);
    }

    #[test]
    fn test_linear_nice() {
        assert_eq!(linear_nice((0.0, 10.0), 10), Some((0.0, 10.0)));
        assert_eq!(linear_nice((0.0, 10.0), 17), Some((0.0, 10.0)));
        assert_eq!(linear_nice((0.4868, 10.85), 10), Some((0.0, 11.0)));
    }
}
