use crate::SectionId;
use crate::constants;
use crate::write::{Address, Error, Result, Writer};

/// A relocation to be applied to a section.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Relocation {
    /// The offset within the section where the relocation should be applied.
    pub offset: usize,
    /// The size of the value to be relocated.
    pub size: u8,
    /// The target of the relocation.
    pub target: RelocationTarget,
    /// The addend to be applied to the relocated value.
    pub addend: i64,
    /// The pointer encoding for relocations in unwind information.
    pub eh_pe: Option<constants::DwEhPe>,
}

/// The target of a relocation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelocationTarget {
    /// The relocation target is a symbol.
    ///
    /// The meaning of this value is decided by the writer, but
    /// will typically be an index into a symbol table.
    Symbol(usize),
    /// The relocation target is a section.
    Section(SectionId),
}

/// A `Writer` which also records relocations.
pub trait RelocateWriter {
    /// The type of the writer being used to write the section data.
    type Writer: Writer;

    /// Get the writer being used to write the section data.
    fn writer(&self) -> &Self::Writer;

    /// Get the writer being used to write the section data.
    fn writer_mut(&mut self) -> &mut Self::Writer;

    /// Record a relocation.
    fn relocate(&mut self, relocation: Relocation);
}

impl<T: RelocateWriter> Writer for T {
    type Endian = <<T as RelocateWriter>::Writer as Writer>::Endian;

    fn endian(&self) -> Self::Endian {
        self.writer().endian()
    }

    fn len(&self) -> usize {
        self.writer().len()
    }

    fn write(&mut self, bytes: &[u8]) -> Result<()> {
        self.writer_mut().write(bytes)
    }

    fn write_at(&mut self, offset: usize, bytes: &[u8]) -> Result<()> {
        self.writer_mut().write_at(offset, bytes)
    }

    fn write_address(&mut self, address: Address, size: u8) -> Result<()> {
        match address {
            Address::Constant(val) => self.writer_mut().write_udata(val, size),
            Address::Symbol { symbol, addend } => {
                self.relocate(Relocation {
                    offset: self.len(),
                    size,
                    target: RelocationTarget::Symbol(symbol),
                    addend,
                    eh_pe: None,
                });
                self.writer_mut().write_udata(0, size)
            }
        }
    }

    fn write_offset(&mut self, val: usize, section: SectionId, size: u8) -> Result<()> {
        self.relocate(Relocation {
            offset: self.len(),
            size,
            target: RelocationTarget::Section(section),
            addend: val as i64,
            eh_pe: None,
        });
        self.writer_mut().write_udata(0, size)
    }

    fn write_offset_at(
        &mut self,
        offset: usize,
        val: usize,
        section: SectionId,
        size: u8,
    ) -> Result<()> {
        self.relocate(Relocation {
            offset,
            size,
            target: RelocationTarget::Section(section),
            addend: val as i64,
            eh_pe: None,
        });
        self.writer_mut().write_udata_at(offset, 0, size)
    }

    fn write_eh_pointer(
        &mut self,
        address: Address,
        eh_pe: constants::DwEhPe,
        size: u8,
    ) -> Result<()> {
        match address {
            Address::Constant(_) => self.writer_mut().write_eh_pointer(address, eh_pe, size),
            Address::Symbol { symbol, addend } => {
                let size = match eh_pe.format() {
                    constants::DW_EH_PE_absptr => size,
                    constants::DW_EH_PE_udata2 => 2,
                    constants::DW_EH_PE_udata4 => 4,
                    constants::DW_EH_PE_udata8 => 8,
                    constants::DW_EH_PE_sdata2 => 2,
                    constants::DW_EH_PE_sdata4 => 4,
                    constants::DW_EH_PE_sdata8 => 8,
                    _ => return Err(Error::UnsupportedPointerEncoding(eh_pe)),
                };
                self.relocate(Relocation {
                    offset: self.len(),
                    size,
                    target: RelocationTarget::Symbol(symbol),
                    addend,
                    eh_pe: Some(eh_pe),
                });
                self.writer_mut().write_udata(0, size)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::write::EndianVec;
    use crate::{LittleEndian, SectionId};
    use alloc::vec::Vec;

    struct Section {
        writer: EndianVec<LittleEndian>,
        relocations: Vec<Relocation>,
    }

    impl RelocateWriter for Section {
        type Writer = EndianVec<LittleEndian>;

        fn writer(&self) -> &Self::Writer {
            &self.writer
        }

        fn writer_mut(&mut self) -> &mut Self::Writer {
            &mut self.writer
        }

        fn relocate(&mut self, relocation: Relocation) {
            self.relocations.push(relocation);
        }
    }

    #[test]
    fn test_relocate_writer() {
        let mut expected_data = Vec::new();
        let mut expected_relocations = Vec::new();

        let mut section = Section {
            writer: EndianVec::new(LittleEndian),
            relocations: Vec::new(),
        };

        // No relocation for plain data.
        section.write_udata(0x12345678, 4).unwrap();
        expected_data.extend_from_slice(&0x12345678u32.to_le_bytes());

        // No relocation for a constant address.
        section
            .write_address(Address::Constant(0x87654321), 4)
            .unwrap();
        expected_data.extend_from_slice(&0x87654321u32.to_le_bytes());

        // Relocation for a symbol address.
        let offset = section.len();
        section
            .write_address(
                Address::Symbol {
                    symbol: 1,
                    addend: 0x12345678,
                },
                4,
            )
            .unwrap();
        expected_data.extend_from_slice(&[0; 4]);
        expected_relocations.push(Relocation {
            offset,
            size: 4,
            target: RelocationTarget::Symbol(1),
            addend: 0x12345678,
            eh_pe: None,
        });

        // Relocation for a section offset.
        let offset = section.len();
        section
            .write_offset(0x12345678, SectionId::DebugAbbrev, 4)
            .unwrap();
        expected_data.extend_from_slice(&[0; 4]);
        expected_relocations.push(Relocation {
            offset,
            size: 4,
            target: RelocationTarget::Section(SectionId::DebugAbbrev),
            addend: 0x12345678,
            eh_pe: None,
        });

        // Relocation for a section offset at a specific offset.
        let offset = section.len();
        section.write_udata(0x12345678, 4).unwrap();
        section
            .write_offset_at(offset, 0x12345678, SectionId::DebugStr, 4)
            .unwrap();
        expected_data.extend_from_slice(&[0; 4]);
        expected_relocations.push(Relocation {
            offset,
            size: 4,
            target: RelocationTarget::Section(SectionId::DebugStr),
            addend: 0x12345678,
            eh_pe: None,
        });

        // No relocation for a constant in unwind information.
        section
            .write_eh_pointer(Address::Constant(0x87654321), constants::DW_EH_PE_absptr, 8)
            .unwrap();
        expected_data.extend_from_slice(&0x87654321u64.to_le_bytes());

        // No relocation for a relative constant in unwind information.
        let offset = section.len();
        section
            .write_eh_pointer(
                Address::Constant(offset as u64 - 8),
                constants::DW_EH_PE_pcrel | constants::DW_EH_PE_sdata4,
                8,
            )
            .unwrap();
        expected_data.extend_from_slice(&(-8i32).to_le_bytes());

        // Relocation for a symbol in unwind information.
        let offset = section.len();
        section
            .write_eh_pointer(
                Address::Symbol {
                    symbol: 2,
                    addend: 0x12345678,
                },
                constants::DW_EH_PE_pcrel | constants::DW_EH_PE_sdata4,
                8,
            )
            .unwrap();
        expected_data.extend_from_slice(&[0; 4]);
        expected_relocations.push(Relocation {
            offset,
            size: 4,
            target: RelocationTarget::Symbol(2),
            addend: 0x12345678,
            eh_pe: Some(constants::DW_EH_PE_pcrel | constants::DW_EH_PE_sdata4),
        });

        assert_eq!(section.writer.into_vec(), expected_data);
        assert_eq!(section.relocations, expected_relocations);
    }
}
