// https://docs.microsoft.com/en-us/typography/opentype/spec/os2

use crate::LineMetrics;
use crate::parser::Stream;


const US_WEIGHT_CLASS_OFFSET: usize = 4;
const US_WIDTH_CLASS_OFFSET: usize = 6;
const Y_SUBSCRIPT_X_SIZE_OFFSET: usize = 10;
const Y_SUPERSCRIPT_X_SIZE_OFFSET: usize = 18;
const Y_STRIKEOUT_SIZE_OFFSET: usize = 26;
const Y_STRIKEOUT_POSITION_OFFSET: usize = 28;
const FS_SELECTION_OFFSET: usize = 62;
const S_TYPO_ASCENDER_OFFSET: usize = 68;
const S_TYPO_DESCENDER_OFFSET: usize = 70;
const S_TYPO_LINE_GAP_OFFSET: usize = 72;
const SX_HEIGHT_OFFSET: usize = 86;


/// A font [weight](https://docs.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass).
#[derive(Clone, Copy, PartialEq, Debug)]
#[allow(missing_docs)]
pub enum Weight {
    Thin,
    ExtraLight,
    Light,
    Normal,
    Medium,
    SemiBold,
    Bold,
    ExtraBold,
    Black,
    Other(u16),
}

impl Weight {
    /// Returns a numeric representation of a weight.
    #[inline]
    pub fn to_number(self) -> u16 {
        match self {
            Weight::Thin        => 100,
            Weight::ExtraLight  => 200,
            Weight::Light       => 300,
            Weight::Normal      => 400,
            Weight::Medium      => 500,
            Weight::SemiBold    => 600,
            Weight::Bold        => 700,
            Weight::ExtraBold   => 800,
            Weight::Black       => 900,
            Weight::Other(n)    => n,
        }
    }
}

impl From<u16> for Weight {
    #[inline]
    fn from(value: u16) -> Self {
        match value {
            100 => Weight::Thin,
            200 => Weight::ExtraLight,
            300 => Weight::Light,
            400 => Weight::Normal,
            500 => Weight::Medium,
            600 => Weight::SemiBold,
            700 => Weight::Bold,
            800 => Weight::ExtraBold,
            900 => Weight::Black,
            _   => Weight::Other(value),
        }
    }
}

impl Default for Weight {
    #[inline]
    fn default() -> Self {
        Weight::Normal
    }
}


/// A font [width](https://docs.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass).
#[derive(Clone, Copy, PartialEq, PartialOrd, Debug)]
#[allow(missing_docs)]
pub enum Width {
    UltraCondensed,
    ExtraCondensed,
    Condensed,
    SemiCondensed,
    Normal,
    SemiExpanded,
    Expanded,
    ExtraExpanded,
    UltraExpanded,
}

impl Width {
    /// Returns a numeric representation of a width.
    #[inline]
    pub fn to_number(self) -> u16 {
        match self {
            Width::UltraCondensed   => 1,
            Width::ExtraCondensed   => 2,
            Width::Condensed        => 3,
            Width::SemiCondensed    => 4,
            Width::Normal           => 5,
            Width::SemiExpanded     => 6,
            Width::Expanded         => 7,
            Width::ExtraExpanded    => 8,
            Width::UltraExpanded    => 9,
        }
    }
}

impl Default for Width {
    #[inline]
    fn default() -> Self {
        Width::Normal
    }
}


/// A script metrics used by subscript and superscript.
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct ScriptMetrics {
    /// Horizontal font size.
    pub x_size: i16,

    /// Vertical font size.
    pub y_size: i16,

    /// X offset.
    pub x_offset: i16,

    /// Y offset.
    pub y_offset: i16,
}


// https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fsselection
#[derive(Clone, Copy)]
struct SelectionFlags(u16);

impl SelectionFlags {
    #[inline] fn italic(self) -> bool { self.0 & (1 << 0) != 0 }
    #[inline] fn bold(self) -> bool { self.0 & (1 << 5) != 0 }
    #[inline] fn regular(self) -> bool { self.0 & (1 << 6) != 0 }
    #[inline] fn use_typo_metrics(self) -> bool { self.0 & (1 << 7) != 0 }
    #[inline] fn oblique(self) -> bool { self.0 & (1 << 9) != 0 }
}


#[derive(Clone, Copy)]
pub(crate) struct Table<'a> {
    version: u8,
    data: &'a [u8],
}

impl<'a> Table<'a> {
    pub fn parse(data: &'a [u8]) -> Option<Self> {
        let mut s = Stream::new(data);
        let version: u16 = s.read()?;

        let table_len = match version {
            0 => 78,
            1 => 86,
            2 => 96,
            3 => 96,
            4 => 96,
            5 => 100,
            _ => return None,
        };

        if data.len() != table_len {
            return None;
        }

        Some(Table {
            version: version as u8,
            data,
        })
    }

    #[inline]
    pub fn weight(&self) -> Weight {
        Weight::from(Stream::read_at(self.data, US_WEIGHT_CLASS_OFFSET).unwrap_or(0))
    }

    #[inline]
    pub fn width(&self) -> Width {
        match Stream::read_at(self.data, US_WIDTH_CLASS_OFFSET).unwrap_or(0) {
            1 => Width::UltraCondensed,
            2 => Width::ExtraCondensed,
            3 => Width::Condensed,
            4 => Width::SemiCondensed,
            5 => Width::Normal,
            6 => Width::SemiExpanded,
            7 => Width::Expanded,
            8 => Width::ExtraExpanded,
            9 => Width::UltraExpanded,
            _ => Width::Normal,
        }
    }

    #[inline]
    pub fn is_regular(&self) -> bool {
        SelectionFlags(self.fs_selection()).regular()
    }

    #[inline]
    pub fn is_italic(&self) -> bool {
        SelectionFlags(self.fs_selection()).italic()
    }

    #[inline]
    pub fn is_bold(&self) -> bool {
        SelectionFlags(self.fs_selection()).bold()
    }

    #[inline]
    pub fn is_oblique(&self) -> bool {
        if self.version < 4 {
            false
        } else {
            SelectionFlags(self.fs_selection()).oblique()
        }
    }

    #[inline]
    pub(crate) fn is_use_typo_metrics(&self) -> bool {
        if self.version < 4 {
            false
        } else {
            SelectionFlags(self.fs_selection()).use_typo_metrics()
        }
    }

    #[inline]
    pub fn x_height(&self) -> Option<i16> {
        if self.version < 2 {
            None
        } else {
            // We cannot use SafeStream here, because x height is an optional data.
            Stream::read_at(self.data, SX_HEIGHT_OFFSET)
        }
    }

    #[inline]
    pub fn strikeout_metrics(&self) -> LineMetrics {
        LineMetrics {
            thickness: Stream::read_at(self.data, Y_STRIKEOUT_SIZE_OFFSET).unwrap_or(0),
            position: Stream::read_at(self.data, Y_STRIKEOUT_POSITION_OFFSET).unwrap_or(0),
        }
    }

    #[inline]
    pub fn subscript_metrics(&self) -> ScriptMetrics {
        let mut s = Stream::new_at(self.data, Y_SUBSCRIPT_X_SIZE_OFFSET).unwrap_or_default();
        ScriptMetrics {
            x_size: s.read().unwrap_or(0),
            y_size: s.read().unwrap_or(0),
            x_offset: s.read().unwrap_or(0),
            y_offset: s.read().unwrap_or(0),
        }
    }

    #[inline]
    pub fn superscript_metrics(&self) -> ScriptMetrics {
        let mut s = Stream::new_at(self.data, Y_SUPERSCRIPT_X_SIZE_OFFSET).unwrap_or_default();
        ScriptMetrics {
            x_size: s.read().unwrap_or(0),
            y_size: s.read().unwrap_or(0),
            x_offset: s.read().unwrap_or(0),
            y_offset: s.read().unwrap_or(0),
        }
    }

    #[inline]
    pub fn typo_ascender(&self) -> i16 {
        Stream::read_at(self.data, S_TYPO_ASCENDER_OFFSET).unwrap_or(0)
    }

    #[inline]
    pub fn typo_descender(&self) -> i16 {
        Stream::read_at(self.data, S_TYPO_DESCENDER_OFFSET).unwrap_or(0)
    }

    #[inline]
    pub fn typo_line_gap(&self) -> i16 {
        Stream::read_at(self.data, S_TYPO_LINE_GAP_OFFSET).unwrap_or(0)
    }

    #[inline]
    fn fs_selection(&self) -> u16 {
        Stream::read_at(self.data, FS_SELECTION_OFFSET).unwrap_or(0)
    }
}
