// SPDX-FileCopyrightText: 2023 - 2024 Markus Haug (Korrat)
//
// SPDX-License-Identifier: EUPL-1.2

use core::fmt;
use core::fmt::Display;
use core::fmt::Formatter;
use core::hash::Hash;

use beancount_amount::Amount;
use beancount_commodity::Commodity;
use rust_decimal::Decimal;
use time::Date;

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum CostBasis {
    Empty,
    PerUnit(Amount),
    Total(Amount),
    PerUnitAndFixed {
        per_unit: Decimal,
        total: Decimal,
        commodity: Commodity,
    },
}

impl CostBasis {
    #[must_use]
    pub const fn commodity(&self) -> Option<Commodity> {
        match self {
            Self::Empty => None,
            Self::PerUnit(amount) | Self::Total(amount) => Some(amount.commodity),
            Self::PerUnitAndFixed { commodity, .. } => Some(*commodity),
        }
    }

    #[must_use]
    pub const fn per_unit(&self) -> Option<Amount> {
        match self {
            Self::Empty | Self::Total(_) => None,
            Self::PerUnit(amount) => Some(*amount),
            Self::PerUnitAndFixed {
                per_unit,
                commodity,
                ..
            } => Some(Amount::new(*per_unit, *commodity)),
        }
    }

    #[must_use]
    pub const fn total(&self) -> Option<Amount> {
        match self {
            Self::Empty | Self::PerUnit(_) => None,
            Self::Total(amount) => Some(*amount),
            Self::PerUnitAndFixed {
                total, commodity, ..
            } => Some(Amount::new(*total, *commodity)),
        }
    }
}

impl Display for CostBasis {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => write!(f, "{{}}"),
            Self::PerUnit(amount) => write!(f, "{{{amount}}}"),
            Self::Total(amount) => write!(f, "{{{{{amount}}}}}"),
            Self::PerUnitAndFixed {
                per_unit,
                total,
                commodity,
            } => write!(f, "{{{per_unit} # {total} {commodity}}}"),
        }
    }
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct CostSpec {
    pub basis: CostBasis,

    pub date: Option<Date>,

    pub label: Option<String>,
}

impl CostSpec {
    #[must_use]
    pub const fn dated(date: Date) -> Self {
        Self {
            basis: CostBasis::Empty,
            date: Some(date),
            label: None,
        }
    }

    #[must_use]
    pub const fn empty() -> Self {
        Self {
            basis: CostBasis::Empty,
            date: None,
            label: None,
        }
    }

    #[must_use]
    pub const fn from_per_unit(per_unit: Decimal, commodity: Commodity) -> Self {
        Self {
            basis: CostBasis::PerUnit(Amount::new(per_unit, commodity)),
            date: None,
            label: None,
        }
    }

    #[must_use]
    pub const fn from_per_unit_and_fixed(
        per_unit: Decimal,
        total: Decimal,
        commodity: Commodity,
    ) -> Self {
        Self {
            basis: CostBasis::PerUnitAndFixed {
                per_unit,
                total,
                commodity,
            },
            date: None,
            label: None,
        }
    }

    #[must_use]
    pub const fn from_total(total: Decimal, commodity: Commodity) -> Self {
        Self {
            basis: CostBasis::Total(Amount::new(total, commodity)),
            date: None,
            label: None,
        }
    }

    #[must_use]
    pub fn labelled(label: impl Into<String>) -> Self {
        Self {
            basis: CostBasis::Empty,
            date: None,
            label: Some(label.into()),
        }
    }
}

impl CostSpec {
    pub fn set_label(&mut self, label: impl Into<String>) -> &mut Self {
        self.label = Some(label.into());
        self
    }
}

impl CostSpec {
    #![allow(clippy::inline_always)]

    delegate::delegate! {
        to self.basis {
            #[must_use]
            pub const fn commodity(&self) -> Option<Commodity>;

            #[must_use]
            pub const fn per_unit(&self) -> Option<Amount>;

            #[must_use]
            pub const fn total(&self) -> Option<Amount>;
        }
    }
}

impl Display for CostSpec {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let mut had_output = false;
        let Self { basis, date, label } = self;

        f.write_str("{")?;

        match basis {
            CostBasis::Empty => {}
            CostBasis::PerUnit(amount) => {
                write!(f, "{amount}")?;
                had_output = true;
            }
            CostBasis::Total(amount) => {
                write!(f, "# {amount}")?;
                had_output = true;
            }
            CostBasis::PerUnitAndFixed {
                per_unit,
                total,
                commodity,
            } => {
                write!(f, "{per_unit} # {total} {commodity}")?;
                had_output = true;
            }
        }

        if let Some(date) = date {
            if had_output {
                f.write_str(", ")?;
            }
            write!(f, "{date}")?;
        }

        if let Some(label) = label {
            if had_output {
                f.write_str(", ")?;
            }
            write!(f, "{label:?}")?;
        }

        f.write_str("}")
    }
}

impl From<CostBasis> for CostSpec {
    fn from(basis: CostBasis) -> Self {
        Self {
            basis,
            date: None,
            label: None,
        }
    }
}