//! The projection descriptor (`wai.meta.projection`; extensions/projection.md): how the
//! pictures of one video rendition map onto the viewer's sphere or field of view, and how
//! its two views are arranged. A `WPJ1` record of 40 bytes, little-endian.

use super::Refusal;
use serde_json::{json, Value};

pub const MAGIC: &[u8; 4] = b"WPJ1";
pub const LEN: usize = 40;
/// One degree in the descriptor's angle unit, 2⁻¹⁶ degrees (ITU-T H.274 §8.15).
pub const DEG: i64 = 1 << 16;

pub const PROJECTIONS: [&str; 4] = ["rectilinear", "equirectangular", "half-equirectangular", "fisheye"];
pub const STEREO: [&str; 4] = ["mono", "side-by-side", "top-bottom", "layers"];

/// A projection descriptor.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Projection {
    pub projection: u8,
    pub stereo: u8,
    pub first_view: u8,
    pub azimuth_range: u32,
    pub elevation_range: u32,
    pub centre_azimuth: i32,
    pub centre_elevation: i32,
    pub centre_tilt: i32,
    pub baseline_um: u32,
    pub left_layer: u8,
    pub right_layer: u8,
}

impl Projection {
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut b = Vec::with_capacity(LEN);
        b.extend_from_slice(MAGIC);
        b.extend_from_slice(&[1, self.projection, self.stereo, self.first_view]);
        b.extend_from_slice(&self.azimuth_range.to_le_bytes());
        b.extend_from_slice(&self.elevation_range.to_le_bytes());
        b.extend_from_slice(&self.centre_azimuth.to_le_bytes());
        b.extend_from_slice(&self.centre_elevation.to_le_bytes());
        b.extend_from_slice(&self.centre_tilt.to_le_bytes());
        b.extend_from_slice(&self.baseline_um.to_le_bytes());
        b.extend_from_slice(&[self.left_layer, self.right_layer, 0, 0, 0, 0, 0, 0]);
        b
    }

    /// Reads a descriptor and checks it on its own (projection.md §3, §4).
    pub fn read(b: &[u8]) -> Result<Projection, Refusal> {
        if b.len() < 4 || &b[..4] != MAGIC {
            return Err(Refusal::NoSignature);
        }
        if b.len() != LEN {
            return Err(Refusal::LengthMismatch);
        }
        let u32_at = |i: usize| u32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);
        let i32_at = |i: usize| i32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);
        if b[4] != 1 || b[5] > 3 || b[6] > 3 || b[7] > 1 || b[34..].iter().any(|&x| x != 0) {
            return Err(Refusal::ReservedValue);
        }
        let p = Projection {
            projection: b[5],
            stereo: b[6],
            first_view: b[7],
            azimuth_range: u32_at(8),
            elevation_range: u32_at(12),
            centre_azimuth: i32_at(16),
            centre_elevation: i32_at(20),
            centre_tilt: i32_at(24),
            baseline_um: u32_at(28),
            left_layer: b[32],
            right_layer: b[33],
        };
        p.check()?;
        Ok(p)
    }

    fn check(&self) -> Result<(), Refusal> {
        let (az, el) = (i64::from(self.azimuth_range), i64::from(self.elevation_range));
        let ranges_ok = match self.projection {
            0 => (az == 0 && el == 0) || (0 < az && az < 180 * DEG && 0 < el && el < 180 * DEG),
            1 => az == 360 * DEG && el == 180 * DEG,
            2 => az == 180 * DEG && el == 180 * DEG,
            _ => 0 < az && az <= 360 * DEG && 0 < el && el <= 360 * DEG,
        };
        let (ca, ce, ct) = (i64::from(self.centre_azimuth), i64::from(self.centre_elevation), i64::from(self.centre_tilt));
        let centre_ok = (-180 * DEG..180 * DEG).contains(&ca) && (-90 * DEG..=90 * DEG).contains(&ce) && (-180 * DEG..180 * DEG).contains(&ct);
        let views_ok = match self.stereo {
            0 => self.first_view == 0 && self.baseline_um == 0 && self.left_layer == 0 && self.right_layer == 0,
            1 | 2 => self.left_layer == 0 && self.right_layer == 0,
            _ => self.left_layer != self.right_layer && self.left_layer <= 62 && self.right_layer <= 62,
        };
        if ranges_ok && centre_ok && views_ok { Ok(()) } else { Err(Refusal::DescriptorInvalid) }
    }

    pub fn params(&self) -> Value {
        json!({
            "projection": PROJECTIONS[self.projection as usize],
            "stereo": STEREO[self.stereo as usize],
            "first_view": if self.first_view == 0 { "left" } else { "right" },
            "coverage": [self.azimuth_range, self.elevation_range],
            "centre": [self.centre_azimuth, self.centre_elevation, self.centre_tilt],
            "baseline_um": self.baseline_um,
            "layers": if self.stereo == 3 { json!([self.left_layer, self.right_layer]) } else { Value::Null },
        })
    }

    /// The binding check against the rendition the descriptor accompanies, given that
    /// rendition's capability and its probe's parameters (projection.md §5): views in layers
    /// need a multiview base that holds both layers.
    pub fn check_binding(&self, base_capability: &str, base: &Value) -> Result<(), Refusal> {
        if self.stereo != 3 {
            return Ok(());
        }
        if base_capability != crate::codecs::CAP_VIDEO_MVHEVC {
            return Err(Refusal::LayerAbsent);
        }
        let present: Vec<u64> = base["layers_present"].as_array().map(|a| a.iter().filter_map(Value::as_u64).collect()).unwrap_or_default();
        for l in [self.left_layer, self.right_layer] {
            if !present.contains(&u64::from(l)) {
                return Err(Refusal::LayerAbsent);
            }
        }
        Ok(())
    }
}

pub fn probe(b: &[u8]) -> Result<Value, Refusal> {
    Ok(Projection::read(b)?.params())
}
