use crate::{
    error::{ParseTagValueError, ValidationError},
    tag::{
        UnknownTag,
        hls::{TagName, into_inner_tag},
    },
};
use std::borrow::Cow;

/// Corresponds to the `#EXT-X-BYTERANGE` tag.
///
/// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.4.2>
#[derive(Debug, Clone)]
pub struct Byterange<'a> {
    length: u64,
    offset: Option<u64>,
    output_line: Cow<'a, [u8]>, // Used with Writer
    output_line_is_dirty: bool, // If should recalculate output_line
}

impl PartialEq for Byterange<'_> {
    fn eq(&self, other: &Self) -> bool {
        self.length() == other.length() && self.offset() == other.offset()
    }
}

impl<'a> TryFrom<UnknownTag<'a>> for Byterange<'a> {
    type Error = ValidationError;

    fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
        let (length, offset) = tag
            .value()
            .ok_or(ParseTagValueError::UnexpectedEmpty)?
            .try_as_decimal_integer_range()?;
        Ok(Self {
            length,
            offset,
            output_line: Cow::Borrowed(tag.original_input),
            output_line_is_dirty: false,
        })
    }
}

impl<'a> Byterange<'a> {
    /// Construct a new `Byterange` tag.
    pub fn new(length: u64, offset: Option<u64>) -> Self {
        Self {
            length,
            offset,
            output_line: Cow::Owned(calculate_line(length, offset)),
            output_line_is_dirty: false,
        }
    }

    /// Corresponds to the length component of the tag value (`n` in `#EXT-X-BYTERANGE:<n>[@<o>]`).
    ///
    /// See [`Self`] for a link to the HLS documentation for this attribute.
    pub fn length(&self) -> u64 {
        self.length
    }

    /// Corresponds to the offset component of the tag value (`o` in `#EXT-X-BYTERANGE:<n>[@<o>]`).
    ///
    /// See [`Self`] for a link to the HLS documentation for this attribute.
    pub fn offset(&self) -> Option<u64> {
        self.offset
    }

    /// Sets the length component value.
    ///
    /// See [`Self`] for a link to the HLS documentation for this attribute.
    pub fn set_length(&mut self, length: u64) {
        self.length = length;
        self.output_line_is_dirty = true;
    }

    /// Sets the offset component value.
    ///
    /// See [`Self`] for a link to the HLS documentation for this attribute.
    pub fn set_offset(&mut self, offset: u64) {
        self.offset = Some(offset);
        self.output_line_is_dirty = true;
    }

    /// Unsets the offset component value (set it to `None`).
    ///
    /// See [`Self`] for a link to the HLS documentation for this attribute.
    pub fn unset_offset(&mut self) {
        self.offset = None;
        self.output_line_is_dirty = true;
    }

    fn recalculate_output_line(&mut self) {
        self.output_line = Cow::Owned(calculate_line(self.length(), self.offset()));
        self.output_line_is_dirty = false;
    }
}

into_inner_tag!(Byterange);

fn calculate_line(length: u64, offset: Option<u64>) -> Vec<u8> {
    let mut line = format!("#EXT{}:{}", TagName::Byterange.as_str(), length);
    if let Some(offset) = offset {
        line.push_str(format!("@{offset}").as_str());
    }
    line.into_bytes()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tag::{IntoInnerTag, hls::test_macro::mutation_tests};
    use pretty_assertions::assert_eq;

    #[test]
    fn new_with_no_offset_should_be_valid_line() {
        let tag = Byterange::new(1024, None);
        assert_eq!(b"#EXT-X-BYTERANGE:1024", tag.into_inner().value());
    }

    #[test]
    fn new_with_offset_should_be_valid_line() {
        let tag = Byterange::new(1024, Some(512));
        assert_eq!(b"#EXT-X-BYTERANGE:1024@512", tag.into_inner().value());
    }

    mutation_tests!(
        Byterange::new(1024, Some(512)),
        (length, 100, @Attr=":100"),
        (offset, @Option 200, @Attr="@200")
    );
}
