use crate::consts::BYTES_PER_LENGTH_OFFSET;

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Size {
    Fixed { size: usize },
    Variable { minimum_size: usize },
}

impl Size {
    #[must_use]
    pub const fn for_untagged_union<const N: usize>(sizes: [Self; N]) -> Self {
        let mut size = sizes[0];
        let mut index = 1;

        while index < sizes.len() {
            size = size.untagged_union(sizes[index]);
            index += 1;
        }

        size
    }

    // The `#[inline]` attribute produces a measurable speedup. This is most likely because the
    // method is used heavily by the container impls generated by `ssz_derive`. Since this is not a
    // generic function, the compiler does not inline it across crate boundaries unless `#[inline]`
    // is present or LTO is enabled.
    #[inline]
    #[must_use]
    pub const fn fixed_part(self) -> usize {
        match self {
            Self::Fixed { size } => size,
            Self::Variable { .. } => BYTES_PER_LENGTH_OFFSET,
        }
    }

    // This cannot be an `Add` impl because the `const_trait_impl` feature is not stable.
    // See <https://github.com/rust-lang/rust/issues/67792>.
    #[inline]
    #[must_use]
    pub const fn add(self, other: Self) -> Self {
        match (self, other) {
            (Self::Fixed { size: self_size }, Self::Fixed { size: other_size }) => Self::Fixed {
                size: self_size + other_size,
            },
            (Self::Fixed { size }, Self::Variable { minimum_size })
            | (Self::Variable { minimum_size }, Self::Fixed { size }) => Self::Variable {
                minimum_size: size + minimum_size,
            },
            (
                Self::Variable {
                    minimum_size: self_minimum_size,
                },
                Self::Variable {
                    minimum_size: other_minimum_size,
                },
            ) => Self::Variable {
                minimum_size: self_minimum_size + other_minimum_size,
            },
        }
    }

    // This cannot be a `Mul` impl because the `const_trait_impl` feature is not stable.
    // See <https://github.com/rust-lang/rust/issues/67792>.
    #[inline]
    #[must_use]
    pub const fn mul(self, scalar: usize) -> Self {
        match self {
            Self::Fixed { size } => Self::Fixed {
                size: scalar * size,
            },
            Self::Variable { minimum_size } => Self::Variable {
                minimum_size: scalar * minimum_size,
            },
        }
    }

    #[inline]
    #[must_use]
    pub const fn untagged_union(self, other: Self) -> Self {
        match (self, other) {
            (Self::Fixed { size: self_size }, Self::Fixed { size: other_size }) => {
                if self_size == other_size {
                    Self::Fixed { size: self_size }
                } else {
                    Self::Variable {
                        minimum_size: min_usize(self_size, other_size),
                    }
                }
            }
            (Self::Fixed { size }, Self::Variable { minimum_size })
            | (Self::Variable { minimum_size }, Self::Fixed { size }) => Self::Variable {
                minimum_size: min_usize(size, minimum_size),
            },
            (
                Self::Variable {
                    minimum_size: self_minimum_size,
                },
                Self::Variable {
                    minimum_size: other_minimum_size,
                },
            ) => Self::Variable {
                minimum_size: min_usize(self_minimum_size, other_minimum_size),
            },
        }
    }

    #[inline]
    #[must_use]
    pub const fn get(self) -> usize {
        match self {
            Self::Fixed { size } => size,
            Self::Variable { minimum_size } => minimum_size,
        }
    }
}

const fn min_usize(a: usize, b: usize) -> usize {
    if a < b {
        a
    } else {
        b
    }
}

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

    use super::*;

    #[test_case([Size::Fixed { size: 1 }, Size::Fixed { size: 1 }] => Size::Fixed { size: 1 })]
    #[test_case([Size::Fixed { size: 2 }, Size::Fixed { size: 3 }] => Size::Variable { minimum_size: 2 })]
    #[test_case([Size::Fixed { size: 3 }, Size::Fixed { size: 2 }] => Size::Variable { minimum_size: 2 })]
    #[test_case([Size::Fixed { size: 4 }, Size::Variable { minimum_size: 5 }] => Size::Variable { minimum_size: 4 })]
    #[test_case([Size::Fixed { size: 5 }, Size::Variable { minimum_size: 4 }] => Size::Variable { minimum_size: 4 })]
    #[test_case([Size::Variable { minimum_size: 6 }, Size::Variable { minimum_size: 7 }] => Size::Variable { minimum_size: 6 })]
    #[test_case([Size::Variable { minimum_size: 7 }, Size::Variable { minimum_size: 6 }] => Size::Variable { minimum_size: 6 })]
    fn size_untagged_union_determines_minimum_size<const N: usize>(sizes: [Size; N]) -> Size {
        Size::for_untagged_union(sizes)
    }
}
