use alloc::vec::Vec;
use core::ops::{Deref, DerefMut};

use crate::common::{Encoding, LocationListsOffset, SectionId};
use crate::write::{
    Address, BaseId, DebugInfoFixup, Error, Expression, FnvIndexSet, Result, Section, Sections,
    UnitOffsets, Writer,
};

define_section!(
    DebugLoc,
    LocationListsOffset,
    "A writable `.debug_loc` section."
);
define_section!(
    DebugLocLists,
    LocationListsOffset,
    "A writable `.debug_loclists` section."
);

define_offsets!(
    LocationListOffsets: LocationListId => LocationListsOffset,
    "The section offsets of a series of location lists within the `.debug_loc` or `.debug_loclists` sections."
);

define_id!(
    LocationListId,
    "An identifier for a location list in a `LocationListTable`."
);

/// A table of location lists that will be stored in a `.debug_loc` or `.debug_loclists` section.
#[derive(Debug, Default)]
pub struct LocationListTable {
    base_id: BaseId,
    locations: FnvIndexSet<LocationList>,
}

impl LocationListTable {
    /// Add a location list to the table.
    pub fn add(&mut self, loc_list: LocationList) -> LocationListId {
        let (index, _) = self.locations.insert_full(loc_list);
        LocationListId::new(self.base_id, index)
    }

    /// Get a reference to a location list.
    ///
    /// # Panics
    ///
    /// Panics if `id` is invalid.
    #[inline]
    pub fn get(&self, id: LocationListId) -> &LocationList {
        debug_assert_eq!(self.base_id, id.base_id);
        &self.locations[id.index]
    }

    /// Write the location list table to the appropriate section for the given DWARF version.
    pub(crate) fn write<W: Writer>(
        &self,
        sections: &mut Sections<W>,
        encoding: Encoding,
        unit_offsets: Option<&UnitOffsets>,
    ) -> Result<LocationListOffsets> {
        if self.locations.is_empty() {
            return Ok(LocationListOffsets::none());
        }

        match encoding.version {
            2..=4 => self.write_loc(
                &mut sections.debug_loc,
                &mut sections.debug_loc_fixups,
                encoding,
                unit_offsets,
            ),
            5 => self.write_loclists(
                &mut sections.debug_loclists,
                &mut sections.debug_loclists_fixups,
                encoding,
                unit_offsets,
            ),
            _ => Err(Error::UnsupportedVersion(encoding.version)),
        }
    }

    /// Write the location list table to the `.debug_loc` section.
    fn write_loc<W: Writer>(
        &self,
        w: &mut DebugLoc<W>,
        refs: &mut Vec<DebugInfoFixup>,
        encoding: Encoding,
        unit_offsets: Option<&UnitOffsets>,
    ) -> Result<LocationListOffsets> {
        let address_size = encoding.address_size;
        let mut offsets = Vec::new();
        for loc_list in self.locations.iter() {
            offsets.push(w.offset());
            for loc in &loc_list.0 {
                // Note that we must ensure none of the ranges have both begin == 0 and end == 0.
                // We do this by ensuring that begin != end, which is a bit more restrictive
                // than required, but still seems reasonable.
                match *loc {
                    Location::BaseAddress { address } => {
                        let marker = !0 >> (64 - address_size * 8);
                        w.write_udata(marker, address_size)?;
                        w.write_address(address, address_size)?;
                    }
                    Location::OffsetPair {
                        begin,
                        end,
                        ref data,
                    } => {
                        if begin == end {
                            return Err(Error::InvalidRange);
                        }
                        w.write_udata(begin, address_size)?;
                        w.write_udata(end, address_size)?;
                        write_expression(&mut w.0, refs, encoding, unit_offsets, data)?;
                    }
                    Location::StartEnd {
                        begin,
                        end,
                        ref data,
                    } => {
                        if begin == end {
                            return Err(Error::InvalidRange);
                        }
                        w.write_address(begin, address_size)?;
                        w.write_address(end, address_size)?;
                        write_expression(&mut w.0, refs, encoding, unit_offsets, data)?;
                    }
                    Location::StartLength {
                        begin,
                        length,
                        ref data,
                    } => {
                        let end = match begin {
                            Address::Constant(begin) => Address::Constant(begin + length),
                            Address::Symbol { symbol, addend } => Address::Symbol {
                                symbol,
                                addend: addend + length as i64,
                            },
                        };
                        if begin == end {
                            return Err(Error::InvalidRange);
                        }
                        w.write_address(begin, address_size)?;
                        w.write_address(end, address_size)?;
                        write_expression(&mut w.0, refs, encoding, unit_offsets, data)?;
                    }
                    Location::DefaultLocation { .. } => {
                        return Err(Error::InvalidRange);
                    }
                }
            }
            w.write_udata(0, address_size)?;
            w.write_udata(0, address_size)?;
        }
        Ok(LocationListOffsets {
            base_id: self.base_id,
            offsets,
        })
    }

    /// Write the location list table to the `.debug_loclists` section.
    fn write_loclists<W: Writer>(
        &self,
        w: &mut DebugLocLists<W>,
        refs: &mut Vec<DebugInfoFixup>,
        encoding: Encoding,
        unit_offsets: Option<&UnitOffsets>,
    ) -> Result<LocationListOffsets> {
        let mut offsets = Vec::new();

        if encoding.version != 5 {
            return Err(Error::NeedVersion(5));
        }

        let length_offset = w.write_initial_length(encoding.format)?;
        let length_base = w.len();

        w.write_u16(encoding.version)?;
        w.write_u8(encoding.address_size)?;
        w.write_u8(0)?; // segment_selector_size
        w.write_u32(0)?; // offset_entry_count (when set to zero DW_FORM_loclistx can't be used, see section 7.29)
        // FIXME implement DW_FORM_loclistx writing and implement the offset entry list

        for loc_list in self.locations.iter() {
            offsets.push(w.offset());
            for loc in &loc_list.0 {
                match *loc {
                    Location::BaseAddress { address } => {
                        w.write_u8(crate::constants::DW_LLE_base_address.0)?;
                        w.write_address(address, encoding.address_size)?;
                    }
                    Location::OffsetPair {
                        begin,
                        end,
                        ref data,
                    } => {
                        w.write_u8(crate::constants::DW_LLE_offset_pair.0)?;
                        w.write_uleb128(begin)?;
                        w.write_uleb128(end)?;
                        write_expression(&mut w.0, refs, encoding, unit_offsets, data)?;
                    }
                    Location::StartEnd {
                        begin,
                        end,
                        ref data,
                    } => {
                        w.write_u8(crate::constants::DW_LLE_start_end.0)?;
                        w.write_address(begin, encoding.address_size)?;
                        w.write_address(end, encoding.address_size)?;
                        write_expression(&mut w.0, refs, encoding, unit_offsets, data)?;
                    }
                    Location::StartLength {
                        begin,
                        length,
                        ref data,
                    } => {
                        w.write_u8(crate::constants::DW_LLE_start_length.0)?;
                        w.write_address(begin, encoding.address_size)?;
                        w.write_uleb128(length)?;
                        write_expression(&mut w.0, refs, encoding, unit_offsets, data)?;
                    }
                    Location::DefaultLocation { ref data } => {
                        w.write_u8(crate::constants::DW_LLE_default_location.0)?;
                        write_expression(&mut w.0, refs, encoding, unit_offsets, data)?;
                    }
                }
            }

            w.write_u8(crate::constants::DW_LLE_end_of_list.0)?;
        }

        let length = (w.len() - length_base) as u64;
        w.write_initial_length_at(length_offset, length, encoding.format)?;

        Ok(LocationListOffsets {
            base_id: self.base_id,
            offsets,
        })
    }
}

/// A locations list that will be stored in a `.debug_loc` or `.debug_loclists` section.
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct LocationList(pub Vec<Location>);

/// A single location.
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum Location {
    /// DW_LLE_base_address
    BaseAddress {
        /// Base address.
        address: Address,
    },
    /// DW_LLE_offset_pair
    OffsetPair {
        /// Start of range relative to base address.
        begin: u64,
        /// End of range relative to base address.
        end: u64,
        /// Location description.
        data: Expression,
    },
    /// DW_LLE_start_end
    StartEnd {
        /// Start of range.
        begin: Address,
        /// End of range.
        end: Address,
        /// Location description.
        data: Expression,
    },
    /// DW_LLE_start_length
    StartLength {
        /// Start of range.
        begin: Address,
        /// Length of range.
        length: u64,
        /// Location description.
        data: Expression,
    },
    /// DW_LLE_default_location
    DefaultLocation {
        /// Location description.
        data: Expression,
    },
}

fn write_expression<W: Writer>(
    w: &mut W,
    refs: &mut Vec<DebugInfoFixup>,
    encoding: Encoding,
    unit_offsets: Option<&UnitOffsets>,
    val: &Expression,
) -> Result<()> {
    let size = val.size(encoding, unit_offsets)? as u64;
    if encoding.version <= 4 {
        w.write_udata(size, 2)?;
    } else {
        w.write_uleb128(size)?;
    }
    val.write(w, Some(refs), encoding, unit_offsets)?;
    Ok(())
}

#[cfg(feature = "read")]
mod convert {
    use super::*;

    use crate::read::{self, Reader};
    use crate::write::{ConvertDebugInfoRef, ConvertError, ConvertResult};

    impl LocationList {
        /// Create a location list by reading the data from the give location list iter.
        pub(crate) fn from<R: Reader<Offset = usize>>(
            mut from: read::RawLocListIter<R>,
            from_unit: read::UnitRef<'_, R>,
            convert_address: &dyn Fn(u64) -> Option<Address>,
            convert_debug_info_ref: &dyn ConvertDebugInfoRef,
        ) -> ConvertResult<Self> {
            let convert_expression = |x| {
                Expression::from(
                    x,
                    from_unit.encoding(),
                    Some(from_unit),
                    convert_address,
                    convert_debug_info_ref,
                )
            };
            let convert_address = |x| convert_address(x).ok_or(ConvertError::InvalidAddress);
            let mut have_base_address = from_unit.low_pc != 0;
            let mut loc_list = Vec::new();
            while let Some(from_loc) = from.next()? {
                let loc = match from_loc {
                    read::RawLocListEntry::AddressOrOffsetPair { begin, end, data } => {
                        // This matches the logic in `RangeList::from`. See the comments there.
                        let begin = convert_address(begin)?;
                        let end = convert_address(end)?;
                        let data = convert_expression(data)?;
                        if have_base_address {
                            let (Address::Constant(begin_offset), Address::Constant(end_offset)) =
                                (begin, end)
                            else {
                                return Err(ConvertError::InvalidRangeRelativeAddress);
                            };
                            Location::OffsetPair {
                                begin: begin_offset,
                                end: end_offset,
                                data,
                            }
                        } else {
                            Location::StartEnd { begin, end, data }
                        }
                    }
                    read::RawLocListEntry::BaseAddress { addr } => {
                        have_base_address = true;
                        let address = convert_address(addr)?;
                        Location::BaseAddress { address }
                    }
                    read::RawLocListEntry::BaseAddressx { addr } => {
                        have_base_address = true;
                        let address = convert_address(from_unit.address(addr)?)?;
                        Location::BaseAddress { address }
                    }
                    read::RawLocListEntry::StartxEndx { begin, end, data } => {
                        let begin = convert_address(from_unit.address(begin)?)?;
                        let end = convert_address(from_unit.address(end)?)?;
                        let data = convert_expression(data)?;
                        Location::StartEnd { begin, end, data }
                    }
                    read::RawLocListEntry::StartxLength {
                        begin,
                        length,
                        data,
                    } => {
                        let begin = convert_address(from_unit.address(begin)?)?;
                        let data = convert_expression(data)?;
                        Location::StartLength {
                            begin,
                            length,
                            data,
                        }
                    }
                    read::RawLocListEntry::OffsetPair { begin, end, data } => {
                        let data = convert_expression(data)?;
                        Location::OffsetPair { begin, end, data }
                    }
                    read::RawLocListEntry::StartEnd { begin, end, data } => {
                        let begin = convert_address(begin)?;
                        let end = convert_address(end)?;
                        let data = convert_expression(data)?;
                        Location::StartEnd { begin, end, data }
                    }
                    read::RawLocListEntry::StartLength {
                        begin,
                        length,
                        data,
                    } => {
                        let begin = convert_address(begin)?;
                        let data = convert_expression(data)?;
                        Location::StartLength {
                            begin,
                            length,
                            data,
                        }
                    }
                    read::RawLocListEntry::DefaultLocation { data } => {
                        let data = convert_expression(data)?;
                        Location::DefaultLocation { data }
                    }
                };
                // In some cases, existing data may contain begin == end, filtering
                // these out.
                match loc {
                    Location::StartLength { length: 0, .. } => continue,
                    Location::StartEnd { begin, end, .. } if begin == end => continue,
                    Location::OffsetPair { begin, end, .. } if begin == end => continue,
                    _ => (),
                }
                loc_list.push(loc);
            }
            Ok(LocationList(loc_list))
        }
    }
}

#[cfg(test)]
#[cfg(feature = "read")]
mod tests {
    use super::*;
    use crate::LittleEndian;
    use crate::common::{
        DebugAbbrevOffset, DebugAddrBase, DebugLocListsBase, DebugRngListsBase,
        DebugStrOffsetsBase, Format, UnitSectionOffset,
    };
    use crate::read;
    use crate::write::{EndianVec, NoConvertDebugInfoRef};
    use alloc::sync::Arc;

    #[test]
    fn test_loc_list() {
        let mut expression = Expression::new();
        expression.op_constu(0);

        for &version in &[2, 3, 4, 5] {
            for &address_size in &[4, 8] {
                for &format in &[Format::Dwarf32, Format::Dwarf64] {
                    let encoding = Encoding {
                        format,
                        version,
                        address_size,
                    };

                    let mut loc_list = LocationList(vec![
                        Location::StartLength {
                            begin: Address::Constant(6666),
                            length: 7777,
                            data: expression.clone(),
                        },
                        Location::StartEnd {
                            begin: Address::Constant(4444),
                            end: Address::Constant(5555),
                            data: expression.clone(),
                        },
                        Location::BaseAddress {
                            address: Address::Constant(1111),
                        },
                        Location::OffsetPair {
                            begin: 2222,
                            end: 3333,
                            data: expression.clone(),
                        },
                    ]);
                    if version >= 5 {
                        loc_list.0.push(Location::DefaultLocation {
                            data: expression.clone(),
                        });
                    }

                    let mut locations = LocationListTable::default();
                    let loc_list_id = locations.add(loc_list.clone());

                    let mut sections = Sections::new(EndianVec::new(LittleEndian));
                    let loc_list_offsets = locations.write(&mut sections, encoding, None).unwrap();
                    assert!(sections.debug_loc_fixups.is_empty());
                    assert!(sections.debug_loclists_fixups.is_empty());

                    let read_debug_loc =
                        read::DebugLoc::new(sections.debug_loc.slice(), LittleEndian);
                    let read_debug_loclists =
                        read::DebugLocLists::new(sections.debug_loclists.slice(), LittleEndian);
                    let read_loc = read::LocationLists::new(read_debug_loc, read_debug_loclists);
                    let offset = loc_list_offsets.get(loc_list_id);
                    let read_loc_list = read_loc.raw_locations(offset, encoding).unwrap();

                    let dwarf = read::Dwarf {
                        locations: read_loc,
                        ..Default::default()
                    };
                    let unit = read::Unit {
                        header: read::UnitHeader::new(
                            encoding,
                            0,
                            read::UnitType::Compilation,
                            DebugAbbrevOffset(0),
                            SectionId::DebugInfo,
                            UnitSectionOffset(0),
                            read::EndianSlice::default(),
                        ),
                        abbreviations: Arc::new(read::Abbreviations::default()),
                        name: None,
                        comp_dir: None,
                        low_pc: 0,
                        str_offsets_base: DebugStrOffsetsBase(0),
                        addr_base: DebugAddrBase(0),
                        loclists_base: DebugLocListsBase(0),
                        rnglists_base: DebugRngListsBase(0),
                        line_program: None,
                        dwo_id: None,
                    };
                    let convert_loc_list = LocationList::from(
                        read_loc_list,
                        unit.unit_ref(&dwarf),
                        &|address| Some(Address::Constant(address)),
                        &NoConvertDebugInfoRef,
                    )
                    .unwrap();

                    if version <= 4 {
                        loc_list.0[0] = Location::StartEnd {
                            begin: Address::Constant(6666),
                            end: Address::Constant(6666 + 7777),
                            data: expression.clone(),
                        };
                    }
                    assert_eq!(loc_list, convert_loc_list);
                }
            }
        }
    }
}
