use core::num::NonZeroU32;
use std::collections::BTreeSet;

use serde::{Deserialize, Serialize};

use super::axis::Axis;
use super::geometry::{LayoutPos, LayoutSize};

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum SplitFractionError {
    #[error("split fraction must be finite")]
    NotFinite,
    #[error("split fraction must lie in [0.0, 1.0]")]
    OutOfRange,
}

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum DockStateError {
    #[error("solidworks default requires distinct panel ids; {0} appears more than once")]
    DuplicatePanelId(PanelId),
}

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(try_from = "f32", into = "f32")]
pub struct SplitFraction(f32);

impl SplitFraction {
    pub const HALF: Self = Self::clamped(0.5);

    pub fn new(value: f32) -> Result<Self, SplitFractionError> {
        if !value.is_finite() {
            return Err(SplitFractionError::NotFinite);
        }
        if !(0.0..=1.0).contains(&value) {
            return Err(SplitFractionError::OutOfRange);
        }
        Ok(Self(value))
    }

    #[must_use]
    pub const fn clamped(value: f32) -> Self {
        if !value.is_finite() {
            return Self(0.5);
        }
        let bounded = if value < 0.0 {
            0.0
        } else if value > 1.0 {
            1.0
        } else {
            value
        };
        Self(bounded)
    }

    #[must_use]
    pub const fn value(self) -> f32 {
        self.0
    }
}

impl TryFrom<f32> for SplitFraction {
    type Error = SplitFractionError;
    fn try_from(value: f32) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

impl From<SplitFraction> for f32 {
    fn from(s: SplitFraction) -> Self {
        s.0
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct TabIndex(u16);

impl TabIndex {
    pub const ZERO: Self = Self(0);

    #[must_use]
    pub const fn new(index: u16) -> Self {
        Self(index)
    }

    #[must_use]
    pub const fn get(self) -> u16 {
        self.0
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PanelId(NonZeroU32);

impl PanelId {
    #[must_use]
    pub const fn new(id: NonZeroU32) -> Self {
        Self(id)
    }

    #[must_use]
    pub const fn get(self) -> NonZeroU32 {
        self.0
    }
}

impl core::fmt::Display for PanelId {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "PanelId({})", self.0.get())
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum DockNode {
    Split {
        axis: Axis,
        fraction: SplitFraction,
        a: Box<DockNode>,
        b: Box<DockNode>,
    },
    Tabs {
        tabs: Vec<PanelId>,
        active: TabIndex,
    },
}

impl DockNode {
    #[must_use]
    pub fn tabs(panels: Vec<PanelId>) -> Self {
        Self::Tabs {
            tabs: panels,
            active: TabIndex::ZERO,
        }
    }

    #[must_use]
    pub fn split(axis: Axis, fraction: SplitFraction, a: Self, b: Self) -> Self {
        Self::Split {
            axis,
            fraction,
            a: Box::new(a),
            b: Box::new(b),
        }
    }

    #[must_use]
    pub fn panel_ids(&self) -> Vec<PanelId> {
        match self {
            Self::Tabs { tabs, .. } => tabs.clone(),
            Self::Split { a, b, .. } => a.panel_ids().into_iter().chain(b.panel_ids()).collect(),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FloatingSurface {
    pub root: DockNode,
    pub origin: LayoutPos,
    pub size: LayoutSize,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DockState {
    pub main: DockNode,
    pub floating: Vec<FloatingSurface>,
}

impl DockState {
    #[must_use]
    pub fn new(main: DockNode) -> Self {
        Self {
            main,
            floating: Vec::new(),
        }
    }

    pub fn solidworks_default(
        menu_bar: PanelId,
        feature_tree: PanelId,
        property_pane: PanelId,
        ribbon: PanelId,
        viewport: PanelId,
        status: PanelId,
    ) -> Result<Self, DockStateError> {
        const FEATURE_TREE_RATIO: SplitFraction = SplitFraction::clamped(0.22);
        const VIEWPORT_RATIO: SplitFraction = SplitFraction::clamped(0.78);
        const RIBBON_RATIO: SplitFraction = SplitFraction::clamped(0.10);
        const MENU_RATIO: SplitFraction = SplitFraction::clamped(0.035);
        const STATUS_RATIO: SplitFraction = SplitFraction::clamped(0.97);
        [
            menu_bar,
            feature_tree,
            property_pane,
            ribbon,
            viewport,
            status,
        ]
        .into_iter()
        .try_fold(BTreeSet::<PanelId>::new(), |mut seen, id| {
            if seen.insert(id) {
                Ok(seen)
            } else {
                Err(DockStateError::DuplicatePanelId(id))
            }
        })?;
        let center = DockNode::split(
            Axis::Horizontal,
            FEATURE_TREE_RATIO,
            DockNode::tabs(vec![feature_tree]),
            DockNode::split(
                Axis::Horizontal,
                VIEWPORT_RATIO,
                DockNode::tabs(vec![viewport]),
                DockNode::tabs(vec![property_pane]),
            ),
        );
        let with_ribbon = DockNode::split(
            Axis::Vertical,
            RIBBON_RATIO,
            DockNode::tabs(vec![ribbon]),
            center,
        );
        let with_menu = DockNode::split(
            Axis::Vertical,
            MENU_RATIO,
            DockNode::tabs(vec![menu_bar]),
            with_ribbon,
        );
        let main = DockNode::split(
            Axis::Vertical,
            STATUS_RATIO,
            with_menu,
            DockNode::tabs(vec![status]),
        );
        Ok(Self::new(main))
    }
}
