//! # nexrad2
//! A parser for the NEXRAD II raw datafile format.

use std::error::Error;
use std::fmt::{Display, Formatter};
use std::io;
use std::io::{Cursor, Read, Seek, SeekFrom};
use std::str::Utf8Error;
use log::{debug, trace};
use crate::bzip::BzipDecoder;
use crate::message::{LEGACY_CTM_HEADER_LEN, MESSAGE_BODY_SIZE, MESSAGE_HEADER_SIZE, MessageHeader};

pub mod message;
pub mod bzip;

#[derive(Debug)]
pub struct Nexrad2Chunk {
    pub volume_header_record: VolumeHeaderRecord,
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// The Volume Header Record as defined by 2620010H (Build 19.0) section 7.3.3 Volume Header Record
pub struct VolumeHeaderRecord {
    /// A character constant of which the last 2 characters identify the version.
    /// Versions defined by 262010H Build 19.0:
    /// - `AR2V0002.`: Super Resolution disabled at the RDA (pre RDA Build 12.0)
    /// - `AR2V0003.`: Super Resolution (pre RDA Build 12.0)
    /// - `AR2V0004.`: Recombined Super Resolution
    /// - `AR2V0005.`: Super Resolution disabled at the RDA (RDA Build 12.0 and later)
    /// - `AR2V0006.`: Super Resolution (RDA Build 12.0 and later)
    /// - `AR2V0007.`: Recombined Super Resolution (RDA Build 12.0 and later)
    pub tape_filename: String,
    /// Increases by 1 for each volume of radar data in the queue to a maximum of 999, whereupon it rolls back to 001
    pub extension_number: String,
    /// Date - NEXRAD-modified Julian - days since 1/1/1970 where 1/1/1970 is 1.
    pub date: u32,
    /// Time - milliseconds past midnight
    pub time: u32,
    /// ICAO of the radar
    pub icao: String
}


#[derive(Debug)]
pub enum NexradParseError {
    InvalidHeaderTapeFilename(Utf8Error),
    HeaderReadError(io::Error),
    InvalidHeaderLength(usize),
    InvalidHeaderExtensionNumber(Utf8Error),
    InvalidICAO(Utf8Error),
    FileTooShort,
    DecompressError(io::Error),
    InvalidMetaChunkLength(usize),
    TcmHeaderReadFailed(io::Error),
    TcmChunkReadFailed(io::Error),
    FailedToReadFile(io::Error),
    LdmReadFailed(io::Error)
}
impl Display for NexradParseError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::InvalidHeaderTapeFilename(e) => write!(f, "invalid header tape filename: {}", e),
            Self::HeaderReadError(e) => write!(f, "error reading header from cursor: {}", e),
            Self::InvalidHeaderLength(read) => write!(f, "invalid header length, expected 24 bytes but only read {}", read),
            Self::InvalidHeaderExtensionNumber(e) => write!(f, "invalid header extension number: {}", e),
            Self::InvalidICAO(e) => write!(f, "invalid station icao: {}", e),
            Self::FileTooShort => write!(f, "file too short (failed to fill whole buffer)"),
            Self::DecompressError(e) => write!(f, "decompression error: {}", e),
            Self::InvalidMetaChunkLength(real) => write!(f, "invalid meta chunk length, spec defined as {NEXRAD2_META_CHUNK_FIXED_LENGTH} but got {real}"),
            Self::TcmHeaderReadFailed(e) => write!(f, "tcm message header read failed: {}", e),
            Self::TcmChunkReadFailed(e) => write!(f, "tcm chunk read failed: {}", e),
            Self::FailedToReadFile(e) => write!(f, "failed to read file: {}", e),
            Self::LdmReadFailed(e) => write!(f, "ldm read failed: {}", e)
        }
    }
}
impl Error for NexradParseError {}

pub const NEXRAD2_META_CHUNK_FIXED_LENGTH: usize = 325888;


pub fn parse_nx2_chunk(cursor: &mut (impl Read + Seek)) -> Result<Nexrad2Chunk, NexradParseError> {
    let mut volume_header = [0u8; 24];
    let read = cursor.read(&mut volume_header).map_err(|e| NexradParseError::HeaderReadError(e))?;
    if read != 24 { return Err(NexradParseError::InvalidHeaderLength(read)); }

    let tape_filename_bytes = &volume_header[0..9];
    let tape_filename = std::str::from_utf8(tape_filename_bytes).map_err(|e| NexradParseError::InvalidHeaderTapeFilename(e))?;

    let extension_number_bytes = &volume_header[9..12];
    let extension_number = std::str::from_utf8(extension_number_bytes).map_err(|e| NexradParseError::InvalidHeaderTapeFilename(e))?;

    let days_since_epoch = u32::from_be_bytes(volume_header[12..16].try_into().unwrap());
    let millis_after_midnight = u32::from_be_bytes(volume_header[16..20].try_into().unwrap());

    let icao_bytes = &volume_header[20..24];
    let icao = std::str::from_utf8(icao_bytes).map_err(|e| NexradParseError::InvalidHeaderTapeFilename(e))?;

    let header = VolumeHeaderRecord {
        tape_filename: tape_filename.to_string(),
        extension_number: extension_number.to_string(),
        date: days_since_epoch,
        time: millis_after_midnight,
        icao: icao.to_string(),
    };

    trace!("Loaded - {:#?}", header);

    loop {
        // LDM records
        let mut ldm_size_bytes = [0u8; 4];
        cursor.read_exact(&mut ldm_size_bytes).map_err(|e| NexradParseError::LdmReadFailed(e))?;
        let ldm_size = i32::from_be_bytes(ldm_size_bytes).abs() as usize;

        trace!("Reading LDM record - {} bytes compressed", ldm_size);

        if ldm_size == 0 {
            trace!("Missing LDM record, unseeking size");
            cursor.seek(SeekFrom::Current(-4)).map_err(|e| NexradParseError::LdmReadFailed(e))?;
        }

        let mut compressed_buf = vec![0u8; ldm_size];
        cursor.read_exact(&mut compressed_buf).map_err(|e| NexradParseError::LdmReadFailed(e))?;

        let mut bz_decoder = BzipDecoder::new(Cursor::new(compressed_buf));

        let mut decompressed_buf = vec![];

        io::copy(&mut bz_decoder, &mut decompressed_buf).map_err(|e| NexradParseError::DecompressError(e))?;

        trace!("LDM record decompressed to {} bytes", decompressed_buf.len());

        let mut decompressed = Cursor::new(decompressed_buf);

        let mut messages = 0;

        loop {
            decompressed.seek(SeekFrom::Current(LEGACY_CTM_HEADER_LEN as i64)).map_err(|e| NexradParseError::TcmChunkReadFailed(e))?;

            let mut message_header = [0u8; MESSAGE_HEADER_SIZE];
            match decompressed.read_exact(&mut message_header) {
                Ok(_) => (),
                Err(e) if matches!(e.kind(), io::ErrorKind::UnexpectedEof) => { break; },
                Err(e) => return Err(NexradParseError::TcmChunkReadFailed(e))
            }

            let message_header = MessageHeader {
                message_size: u16::from_be_bytes(message_header[0..2].try_into().unwrap()),
                rda_redundant_channel: message_header[2],
                message_type: message_header[3],
                message_sequence_number: u16::from_be_bytes(message_header[4..6].try_into().unwrap()),
                julian_date: u16::from_be_bytes(message_header[6..8].try_into().unwrap()),
                millis_after_midnight: u32::from_be_bytes(message_header[8..12].try_into().unwrap()),
                num_of_message_segments: u16::from_be_bytes(message_header[12..14].try_into().unwrap()),
                message_segment_num: u16::from_be_bytes(message_header[14..].try_into().unwrap()),
            };

            debug!("Message: {:#?}", message_header);

            let mut body_buf = vec![0u8; MESSAGE_BODY_SIZE];

            decompressed.read_exact(&mut body_buf).map_err(|e| NexradParseError::TcmChunkReadFailed(e))?;

            messages += 1;
        }

        debug!("{} messages loaded from chunk", messages);

        break;
    }

    Ok(Nexrad2Chunk {
        volume_header_record: header
    })
}

