use crate::pcm_audio::*;
use crate::timestamp_validator::*;
use fidl_fuchsia_media::*;
use fidl_fuchsia_sysmem::*;
use fuchsia_zircon as zx;
use hex::encode;
use mundane::hash::{Digest, Hasher, Sha256};
use rand::prelude::*;
use std::io::Write;
use std::rc::Rc;
use stream_processor_encoder_factory::*;
use stream_processor_test::*;

pub const TEST_PCM_FRAME_COUNT: usize = 3000;

// TODO(turnage): Generalize and promote to stream_processor_test lib.
pub struct BytesValidator {
    pub output_file: Option<&'static str>,
    pub expected_digest: ExpectedDigest,
}

impl BytesValidator {
    fn write_and_hash(
        &self,
        mut file: impl Write,
        oob: &[u8],
        packets: &[&OutputPacket],
    ) -> Result<()> {
        let mut hasher = Sha256::default();

        hasher.update(oob);

        for packet in packets {
            file.write_all(&packet.data)?;
            hasher.update(&packet.data);
        }

        let digest = hasher.finish().bytes();
        if self.expected_digest.bytes != digest {
            return Err(FatalError(format!(
                "Expected {}; got {}",
                self.expected_digest,
                encode(digest)
            ))
            .into());
        }

        Ok(())
    }

    fn output_file(&self) -> Result<impl Write> {
        Ok(if let Some(file) = self.output_file {
            Box::new(std::fs::File::create(file)?) as Box<dyn Write>
        } else {
            Box::new(std::io::sink()) as Box<dyn Write>
        })
    }
}

impl OutputValidator for BytesValidator {
    fn validate(&self, output: &[Output]) -> Result<()> {
        let packets: Vec<&OutputPacket> = output_packets(output).collect();
        let oob = packets
            .first()
            .ok_or(FatalError(String::from("No packets in output")))?
            .format
            .format_details
            .oob_bytes
            .clone()
            .unwrap_or(vec![]);

        self.write_and_hash(self.output_file()?, oob.as_slice(), &packets)
    }
}

pub struct AudioEncoderTestCase {
    /// Encoder settings.
    // This is a function because FIDL unions are not Copy or Clone.
    pub settings: Rc<dyn Fn() -> EncoderSettings>,
    /// The number of PCM input frames per encoded frame.
    pub input_framelength: usize,
    pub channel_count: usize,
    pub hash_tests: Vec<AudioEncoderHashTest>,
}

/// A hash test runs audio through the encoder and checks that all that data emitted when hashed
/// sequentially results in the expected digest. Oob bytes are hashed first.
pub struct AudioEncoderHashTest {
    /// If provided, the output will also be written to this file. Use this to verify new files
    /// with a decoder before using their digest in tests.
    pub output_file: Option<&'static str>,
    pub input_format: PcmFormat,
    pub output_packet_count: usize,
    pub expected_digest: ExpectedDigest,
}

impl AudioEncoderTestCase {
    pub async fn run(self) -> Result<()> {
        await!(self.test_termination())?;
        await!(self.test_timestamps())?;
        await!(self.test_hashes())
    }

    async fn test_hashes(self) -> Result<()> {
        let mut cases = vec![];
        let easy_framelength = self.input_framelength;
        for (hash_test, stream_lifetime_ordinal) in
            self.hash_tests.into_iter().zip(OrdinalPattern::Odd.into_iter())
        {
            let settings = self.settings.clone();
            let pcm_audio = PcmAudio::create_saw_wave(hash_test.input_format, TEST_PCM_FRAME_COUNT);
            let stream = Rc::new(PcmAudioStream {
                pcm_audio,
                encoder_settings: move || (settings)(),
                frames_per_packet: (0..).map(move |_| easy_framelength),
                timebase: None,
            });

            cases.push(TestCase {
                name: "Audio encoder hash test",
                stream,
                validators: vec![
                    Rc::new(TerminatesWithValidator {
                        expected_terminal_output: Output::Eos { stream_lifetime_ordinal },
                    }),
                    Rc::new(OutputPacketCountValidator {
                        expected_output_packet_count: hash_test.output_packet_count,
                    }),
                    Rc::new(BytesValidator {
                        output_file: hash_test.output_file,
                        expected_digest: hash_test.expected_digest,
                    }),
                ],
                stream_options: Some(StreamOptions {
                    queue_format_details: false,
                    ..StreamOptions::default()
                }),
            });
        }

        let spec = TestSpec {
            cases,
            relation: CaseRelation::Serial,
            stream_processor_factory: Rc::new(EncoderFactory),
        };

        await!(spec.run())
    }

    async fn test_termination(&self) -> Result<()> {
        let easy_framelength = self.input_framelength;
        let stream = self.create_test_stream((0..).map(move |_| easy_framelength));
        let eos_validator = Rc::new(TerminatesWithValidator {
            expected_terminal_output: Output::Eos { stream_lifetime_ordinal: 1 },
        });

        let case = TestCase {
            name: "Terminates with EOS test",
            stream,
            validators: vec![eos_validator],
            stream_options: None,
        };

        let spec = TestSpec {
            cases: vec![case],
            relation: CaseRelation::Concurrent,
            stream_processor_factory: Rc::new(EncoderFactory),
        };

        await!(spec.run())
    }

    async fn test_timestamps(&self) -> Result<()> {
        let max_framelength = self.input_framelength * 5;

        let fixed_framelength = self.input_framelength + 1;
        let fixed_framelength_stream =
            self.create_test_stream((0..).map(move |_| fixed_framelength));
        let pcm_frame_size = fixed_framelength_stream.pcm_audio.frame_size();

        let stream_options = Some(StreamOptions {
            input_buffer_collection_constraints: Some(BufferCollectionConstraints {
                has_buffer_memory_constraints: true,
                buffer_memory_constraints: BufferMemoryConstraints {
                    min_size_bytes: (max_framelength * pcm_frame_size) as u32,
                    ..BUFFER_MEMORY_CONSTRAINTS_DEFAULT
                },
                ..BUFFER_COLLECTION_CONSTRAINTS_DEFAULT
            }),
            ..StreamOptions::default()
        });

        let fixed_framelength_case = TestCase {
            name: "Timestamp extrapolation test - fixed framelength",
            validators: vec![Rc::new(TimestampValidator::new(
                self.input_framelength,
                pcm_frame_size,
                fixed_framelength_stream.timestamp_generator(),
                fixed_framelength_stream.as_ref(),
            ))],
            stream: fixed_framelength_stream,
            stream_options,
        };

        let variable_framelength_stream = self.create_test_stream((0..).map(move |i| {
            let mut rng = StdRng::seed_from_u64(i as u64);
            rng.gen::<usize>() % max_framelength + 1
        }));
        let variable_framelength_case = TestCase {
            name: "Timestamp extrapolation test - variable framelength",
            validators: vec![Rc::new(TimestampValidator::new(
                self.input_framelength,
                pcm_frame_size,
                variable_framelength_stream.timestamp_generator(),
                variable_framelength_stream.as_ref(),
            ))],
            stream: variable_framelength_stream,
            stream_options,
        };

        let spec = TestSpec {
            cases: vec![fixed_framelength_case, variable_framelength_case],
            relation: CaseRelation::Concurrent,
            stream_processor_factory: Rc::new(EncoderFactory),
        };

        await!(spec.run())
    }

    fn create_test_stream(
        &self,
        frames_per_packet: impl Iterator<Item = usize> + Clone,
    ) -> Rc<PcmAudioStream<impl Iterator<Item = usize> + Clone, impl Fn() -> EncoderSettings>> {
        let pcm_format = PcmFormat {
            pcm_mode: AudioPcmMode::Linear,
            bits_per_sample: 16,
            frames_per_second: 44100,
            channel_map: match self.channel_count {
                1 => vec![AudioChannelId::Cf],
                2 => vec![AudioChannelId::Lf, AudioChannelId::Rf],
                c => panic!("{} is not a valid channel count", c),
            },
        };
        let pcm_audio = PcmAudio::create_saw_wave(pcm_format.clone(), TEST_PCM_FRAME_COUNT);
        let settings = self.settings.clone();
        Rc::new(PcmAudioStream {
            pcm_audio,
            encoder_settings: move || (settings)(),
            frames_per_packet: frames_per_packet,
            timebase: Some(zx::Duration::from_seconds(1).into_nanos() as u64),
        })
    }
}
