use super::{Instruction, LogicalLayout, PhysicalLayout};
use spirv::{Op, Word, MAGIC_NUMBER};
use std::iter;

// https://github.com/KhronosGroup/SPIRV-Headers/pull/195
const GENERATOR: Word = 28;

impl PhysicalLayout {
    pub(super) const fn new(version: Word) -> Self {
        PhysicalLayout {
            magic_number: MAGIC_NUMBER,
            version,
            generator: GENERATOR,
            bound: 0,
            instruction_schema: 0x0u32,
        }
    }

    pub(super) fn in_words(&self, sink: &mut impl Extend<Word>) {
        sink.extend(iter::once(self.magic_number));
        sink.extend(iter::once(self.version));
        sink.extend(iter::once(self.generator));
        sink.extend(iter::once(self.bound));
        sink.extend(iter::once(self.instruction_schema));
    }
}

impl super::recyclable::Recyclable for PhysicalLayout {
    fn recycle(self) -> Self {
        PhysicalLayout {
            magic_number: self.magic_number,
            version: self.version,
            generator: self.generator,
            instruction_schema: self.instruction_schema,
            bound: 0,
        }
    }
}

impl LogicalLayout {
    pub(super) fn in_words(&self, sink: &mut impl Extend<Word>) {
        sink.extend(self.capabilities.iter().cloned());
        sink.extend(self.extensions.iter().cloned());
        sink.extend(self.ext_inst_imports.iter().cloned());
        sink.extend(self.memory_model.iter().cloned());
        sink.extend(self.entry_points.iter().cloned());
        sink.extend(self.execution_modes.iter().cloned());
        sink.extend(self.debugs.iter().cloned());
        sink.extend(self.annotations.iter().cloned());
        sink.extend(self.declarations.iter().cloned());
        sink.extend(self.function_declarations.iter().cloned());
        sink.extend(self.function_definitions.iter().cloned());
    }
}

impl super::recyclable::Recyclable for LogicalLayout {
    fn recycle(self) -> Self {
        Self {
            capabilities: self.capabilities.recycle(),
            extensions: self.extensions.recycle(),
            ext_inst_imports: self.ext_inst_imports.recycle(),
            memory_model: self.memory_model.recycle(),
            entry_points: self.entry_points.recycle(),
            execution_modes: self.execution_modes.recycle(),
            debugs: self.debugs.recycle(),
            annotations: self.annotations.recycle(),
            declarations: self.declarations.recycle(),
            function_declarations: self.function_declarations.recycle(),
            function_definitions: self.function_definitions.recycle(),
        }
    }
}

impl Instruction {
    pub(super) const fn new(op: Op) -> Self {
        Instruction {
            op,
            wc: 1, // Always start at 1 for the first word (OP + WC),
            type_id: None,
            result_id: None,
            operands: vec![],
        }
    }

    #[allow(clippy::panic)]
    pub(super) fn set_type(&mut self, id: Word) {
        assert!(self.type_id.is_none(), "Type can only be set once");
        self.type_id = Some(id);
        self.wc += 1;
    }

    #[allow(clippy::panic)]
    pub(super) fn set_result(&mut self, id: Word) {
        assert!(self.result_id.is_none(), "Result can only be set once");
        self.result_id = Some(id);
        self.wc += 1;
    }

    pub(super) fn add_operand(&mut self, operand: Word) {
        self.operands.push(operand);
        self.wc += 1;
    }

    pub(super) fn add_operands(&mut self, operands: Vec<Word>) {
        for operand in operands.into_iter() {
            self.add_operand(operand)
        }
    }

    pub(super) fn to_words(&self, sink: &mut impl Extend<Word>) {
        sink.extend(Some(self.wc << 16 | self.op as u32));
        sink.extend(self.type_id);
        sink.extend(self.result_id);
        sink.extend(self.operands.iter().cloned());
    }
}

impl Instruction {
    #[cfg(test)]
    fn validate(&self, words: &[Word]) {
        let mut inst_index = 0;
        let (wc, op) = ((words[inst_index] >> 16) as u16, words[inst_index] as u16);
        inst_index += 1;

        assert_eq!(wc, words.len() as u16);
        assert_eq!(op, self.op as u16);

        if self.type_id.is_some() {
            assert_eq!(words[inst_index], self.type_id.unwrap());
            inst_index += 1;
        }

        if self.result_id.is_some() {
            assert_eq!(words[inst_index], self.result_id.unwrap());
            inst_index += 1;
        }

        for (op_index, i) in (inst_index..wc as usize).enumerate() {
            assert_eq!(words[i], self.operands[op_index]);
        }
    }
}

#[test]
fn test_physical_layout_in_words() {
    let bound = 5;
    let version = 0x10203;

    let mut output = vec![];
    let mut layout = PhysicalLayout::new(version);
    layout.bound = bound;

    layout.in_words(&mut output);

    assert_eq!(&output, &[MAGIC_NUMBER, version, GENERATOR, bound, 0,]);
}

#[test]
fn test_logical_layout_in_words() {
    let mut output = vec![];
    let mut layout = LogicalLayout::default();
    let layout_vectors = 11;
    let mut instructions = Vec::with_capacity(layout_vectors);

    let vector_names = &[
        "Capabilities",
        "Extensions",
        "External Instruction Imports",
        "Memory Model",
        "Entry Points",
        "Execution Modes",
        "Debugs",
        "Annotations",
        "Declarations",
        "Function Declarations",
        "Function Definitions",
    ];

    for (i, _) in vector_names.iter().enumerate().take(layout_vectors) {
        let mut dummy_instruction = Instruction::new(Op::Constant);
        dummy_instruction.set_type((i + 1) as u32);
        dummy_instruction.set_result((i + 2) as u32);
        dummy_instruction.add_operand((i + 3) as u32);
        dummy_instruction.add_operands(super::helpers::string_to_words(
            format!("This is the vector: {}", vector_names[i]).as_str(),
        ));
        instructions.push(dummy_instruction);
    }

    instructions[0].to_words(&mut layout.capabilities);
    instructions[1].to_words(&mut layout.extensions);
    instructions[2].to_words(&mut layout.ext_inst_imports);
    instructions[3].to_words(&mut layout.memory_model);
    instructions[4].to_words(&mut layout.entry_points);
    instructions[5].to_words(&mut layout.execution_modes);
    instructions[6].to_words(&mut layout.debugs);
    instructions[7].to_words(&mut layout.annotations);
    instructions[8].to_words(&mut layout.declarations);
    instructions[9].to_words(&mut layout.function_declarations);
    instructions[10].to_words(&mut layout.function_definitions);

    layout.in_words(&mut output);

    let mut index: usize = 0;
    for instruction in instructions {
        let wc = instruction.wc as usize;
        instruction.validate(&output[index..index + wc]);
        index += wc;
    }
}
