use alloc::fmt;
use core::convert::TryInto;
use core::fmt::Debug;
use core::marker::PhantomData;
use core::str;

use crate::endian::{BigEndian as BE, U32Bytes};
use crate::pod::{bytes_of, Pod};
use crate::read::util::StringTable;
use crate::xcoff;

use crate::read::{
    self, Bytes, Error, ObjectSymbol, ObjectSymbolTable, ReadError, ReadRef, Result, SectionIndex,
    SymbolFlags, SymbolIndex, SymbolKind, SymbolScope, SymbolSection,
};

use super::{FileHeader, XcoffFile};

/// A table of symbol entries in an XCOFF file.
///
/// Also includes the string table used for the symbol names.
#[derive(Debug)]
pub struct SymbolTable<'data, Xcoff, R = &'data [u8]>
where
    Xcoff: FileHeader,
    R: ReadRef<'data>,
{
    symbols: &'data [xcoff::SymbolBytes],
    strings: StringTable<'data, R>,
    header: PhantomData<Xcoff>,
}

impl<'data, Xcoff, R> Default for SymbolTable<'data, Xcoff, R>
where
    Xcoff: FileHeader,
    R: ReadRef<'data>,
{
    fn default() -> Self {
        Self {
            symbols: &[],
            strings: StringTable::default(),
            header: PhantomData,
        }
    }
}

impl<'data, Xcoff, R> SymbolTable<'data, Xcoff, R>
where
    Xcoff: FileHeader,
    R: ReadRef<'data>,
{
    /// Parse the symbol table.
    pub fn parse(header: Xcoff, data: R) -> Result<Self> {
        let mut offset = header.f_symptr().into();
        let (symbols, strings) = if offset != 0 {
            let symbols = data
                .read_slice(&mut offset, header.f_nsyms() as usize)
                .read_error("Invalid XCOFF symbol table offset or size")?;

            // Parse the string table.
            // Note: don't update data when reading length; the length includes itself.
            let length = data
                .read_at::<U32Bytes<_>>(offset)
                .read_error("Missing XCOFF string table")?
                .get(BE);
            let str_end = offset
                .checked_add(length as u64)
                .read_error("Invalid XCOFF string table length")?;
            let strings = StringTable::new(data, offset, str_end);

            (symbols, strings)
        } else {
            (&[][..], StringTable::default())
        };

        Ok(SymbolTable {
            symbols,
            strings,
            header: PhantomData,
        })
    }

    /// Return the symbol entry at the given index and offset.
    pub fn get<T: Pod>(&self, index: usize, offset: usize) -> Result<&'data T> {
        let entry = index
            .checked_add(offset)
            .and_then(|x| self.symbols.get(x))
            .read_error("Invalid XCOFF symbol index")?;
        let bytes = bytes_of(entry);
        Bytes(bytes).read().read_error("Invalid XCOFF symbol data")
    }

    /// Return the symbol at the given index.
    pub fn symbol(&self, index: usize) -> Result<&'data Xcoff::Symbol> {
        self.get::<Xcoff::Symbol>(index, 0)
    }

    /// Return a file auxiliary symbol.
    pub fn aux_file(&self, index: usize, offset: usize) -> Result<&'data Xcoff::FileAux> {
        debug_assert!(self.symbol(index)?.has_aux_file());
        let aux_file = self.get::<Xcoff::FileAux>(index, offset)?;
        if let Some(aux_type) = aux_file.x_auxtype() {
            if aux_type != xcoff::AUX_FILE {
                return Err(Error("Invalid index for file auxiliary symbol."));
            }
        }
        Ok(aux_file)
    }

    /// Return the csect auxiliary symbol.
    pub fn aux_csect(&self, index: usize, offset: usize) -> Result<&'data Xcoff::CsectAux> {
        debug_assert!(self.symbol(index)?.has_aux_csect());
        let aux_csect = self.get::<Xcoff::CsectAux>(index, offset)?;
        if let Some(aux_type) = aux_csect.x_auxtype() {
            if aux_type != xcoff::AUX_CSECT {
                return Err(Error("Invalid index/offset for csect auxiliary symbol."));
            }
        }
        Ok(aux_csect)
    }

    /// Return true if the symbol table is empty.
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.symbols.is_empty()
    }

    /// The number of symbol table entries.
    ///
    /// This includes auxiliary symbol table entries.
    #[inline]
    pub fn len(&self) -> usize {
        self.symbols.len()
    }
}

/// A symbol table of an `XcoffFile32`.
pub type XcoffSymbolTable32<'data, 'file, R = &'data [u8]> =
    XcoffSymbolTable<'data, 'file, xcoff::FileHeader32, R>;
/// A symbol table of an `XcoffFile64`.
pub type XcoffSymbolTable64<'data, 'file, R = &'data [u8]> =
    XcoffSymbolTable<'data, 'file, xcoff::FileHeader64, R>;

/// A symbol table of an `XcoffFile`.
#[derive(Debug, Clone, Copy)]
pub struct XcoffSymbolTable<'data, 'file, Xcoff, R = &'data [u8]>
where
    Xcoff: FileHeader,
    R: ReadRef<'data>,
{
    pub(crate) file: &'file XcoffFile<'data, Xcoff, R>,
    pub(super) symbols: &'file SymbolTable<'data, Xcoff, R>,
}

impl<'data, 'file, Xcoff: FileHeader, R: ReadRef<'data>> read::private::Sealed
    for XcoffSymbolTable<'data, 'file, Xcoff, R>
{
}

impl<'data, 'file, Xcoff: FileHeader, R: ReadRef<'data>> ObjectSymbolTable<'data>
    for XcoffSymbolTable<'data, 'file, Xcoff, R>
{
    type Symbol = XcoffSymbol<'data, 'file, Xcoff, R>;
    type SymbolIterator = XcoffSymbolIterator<'data, 'file, Xcoff, R>;

    fn symbols(&self) -> Self::SymbolIterator {
        XcoffSymbolIterator {
            file: self.file,
            symbols: self.symbols,
            index: 0,
        }
    }

    fn symbol_by_index(&self, index: SymbolIndex) -> read::Result<Self::Symbol> {
        let symbol = self.symbols.symbol(index.0)?;
        Ok(XcoffSymbol {
            file: self.file,
            symbols: self.symbols,
            index,
            symbol,
        })
    }
}

/// An iterator over the symbols of an `XcoffFile32`.
pub type XcoffSymbolIterator32<'data, 'file, R = &'data [u8]> =
    XcoffSymbolIterator<'data, 'file, xcoff::FileHeader32, R>;
/// An iterator over the symbols of an `XcoffFile64`.
pub type XcoffSymbolIterator64<'data, 'file, R = &'data [u8]> =
    XcoffSymbolIterator<'data, 'file, xcoff::FileHeader64, R>;

/// An iterator over the symbols of an `XcoffFile`.
pub struct XcoffSymbolIterator<'data, 'file, Xcoff, R = &'data [u8]>
where
    Xcoff: FileHeader,
    R: ReadRef<'data>,
{
    pub(crate) file: &'file XcoffFile<'data, Xcoff, R>,
    pub(super) symbols: &'file SymbolTable<'data, Xcoff, R>,
    pub(super) index: usize,
}

impl<'data, 'file, Xcoff: FileHeader, R: ReadRef<'data>> fmt::Debug
    for XcoffSymbolIterator<'data, 'file, Xcoff, R>
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("XcoffSymbolIterator").finish()
    }
}

impl<'data, 'file, Xcoff: FileHeader, R: ReadRef<'data>> Iterator
    for XcoffSymbolIterator<'data, 'file, Xcoff, R>
{
    type Item = XcoffSymbol<'data, 'file, Xcoff, R>;

    fn next(&mut self) -> Option<Self::Item> {
        let index = self.index;
        let symbol = self.symbols.symbol(index).ok()?;
        // TODO: skip over the auxiliary symbols for now.
        self.index += 1 + symbol.n_numaux() as usize;
        Some(XcoffSymbol {
            file: self.file,
            symbols: self.symbols,
            index: SymbolIndex(index),
            symbol,
        })
    }
}

/// A symbol of an `XcoffFile32`.
pub type XcoffSymbol32<'data, 'file, R = &'data [u8]> =
    XcoffSymbol<'data, 'file, xcoff::FileHeader32, R>;
/// A symbol of an `XcoffFile64`.
pub type XcoffSymbol64<'data, 'file, R = &'data [u8]> =
    XcoffSymbol<'data, 'file, xcoff::FileHeader64, R>;

/// A symbol of an `XcoffFile`.
#[derive(Debug, Clone, Copy)]
pub struct XcoffSymbol<'data, 'file, Xcoff, R = &'data [u8]>
where
    Xcoff: FileHeader,
    R: ReadRef<'data>,
{
    pub(crate) file: &'file XcoffFile<'data, Xcoff, R>,
    pub(super) symbols: &'file SymbolTable<'data, Xcoff, R>,
    pub(super) index: SymbolIndex,
    pub(super) symbol: &'data Xcoff::Symbol,
}

impl<'data, 'file, Xcoff: FileHeader, R: ReadRef<'data>> read::private::Sealed
    for XcoffSymbol<'data, 'file, Xcoff, R>
{
}

impl<'data, 'file, Xcoff: FileHeader, R: ReadRef<'data>> ObjectSymbol<'data>
    for XcoffSymbol<'data, 'file, Xcoff, R>
{
    #[inline]
    fn index(&self) -> SymbolIndex {
        self.index
    }

    fn name_bytes(&self) -> Result<&'data [u8]> {
        if self.symbol.has_aux_file() {
            // By convention the file name is in the first auxiliary entry.
            self.symbols
                .aux_file(self.index.0, 1)?
                .fname(self.symbols.strings)
        } else {
            self.symbol.name(self.symbols.strings)
        }
    }

    fn name(&self) -> Result<&'data str> {
        let name = self.name_bytes()?;
        str::from_utf8(name)
            .ok()
            .read_error("Non UTF-8 XCOFF symbol name")
    }

    #[inline]
    fn address(&self) -> u64 {
        match self.symbol.n_sclass() {
            // Relocatable address.
            xcoff::C_EXT
            | xcoff::C_WEAKEXT
            | xcoff::C_HIDEXT
            | xcoff::C_FCN
            | xcoff::C_BLOCK
            | xcoff::C_STAT
            | xcoff::C_INFO => self.symbol.n_value().into(),
            _ => 0,
        }
    }

    #[inline]
    fn size(&self) -> u64 {
        if self.symbol.has_aux_csect() {
            // XCOFF32 must have the csect auxiliary entry as the last auxiliary entry.
            // XCOFF64 doesn't require this, but conventionally does.
            if let Ok(aux_csect) = self
                .file
                .symbols
                .aux_csect(self.index.0, self.symbol.n_numaux() as usize)
            {
                let sym_type = aux_csect.sym_type() & 0x07;
                if sym_type == xcoff::XTY_SD || sym_type == xcoff::XTY_CM {
                    return aux_csect.x_scnlen();
                }
            }
        }
        0
    }

    fn kind(&self) -> SymbolKind {
        if self.symbol.has_aux_csect() {
            if let Ok(aux_csect) = self
                .file
                .symbols
                .aux_csect(self.index.0, self.symbol.n_numaux() as usize)
            {
                let sym_type = aux_csect.sym_type() & 0x07;
                if sym_type == xcoff::XTY_SD || sym_type == xcoff::XTY_CM {
                    return match aux_csect.x_smclas() {
                        xcoff::XMC_PR | xcoff::XMC_GL => SymbolKind::Text,
                        xcoff::XMC_RO | xcoff::XMC_RW | xcoff::XMC_TD | xcoff::XMC_BS => {
                            SymbolKind::Data
                        }
                        xcoff::XMC_TL | xcoff::XMC_UL => SymbolKind::Tls,
                        xcoff::XMC_DS | xcoff::XMC_TC0 | xcoff::XMC_TC => {
                            // `Metadata` might be a better kind for these if we had it.
                            SymbolKind::Data
                        }
                        _ => SymbolKind::Unknown,
                    };
                } else if sym_type == xcoff::XTY_LD {
                    // A function entry point. Neither `Text` nor `Label` are a good fit for this.
                    return SymbolKind::Text;
                } else if sym_type == xcoff::XTY_ER {
                    return SymbolKind::Unknown;
                }
            }
        }
        match self.symbol.n_sclass() {
            xcoff::C_NULL => SymbolKind::Null,
            xcoff::C_FILE => SymbolKind::File,
            _ => SymbolKind::Unknown,
        }
    }

    fn section(&self) -> SymbolSection {
        match self.symbol.n_scnum() {
            xcoff::N_ABS => SymbolSection::Absolute,
            xcoff::N_UNDEF => SymbolSection::Undefined,
            xcoff::N_DEBUG => SymbolSection::None,
            index if index > 0 => SymbolSection::Section(SectionIndex(index as usize)),
            _ => SymbolSection::Unknown,
        }
    }

    #[inline]
    fn is_undefined(&self) -> bool {
        self.symbol.is_undefined()
    }

    /// Return true if the symbol is a definition of a function or data object.
    #[inline]
    fn is_definition(&self) -> bool {
        if self.symbol.has_aux_csect() {
            if let Ok(aux_csect) = self
                .symbols
                .aux_csect(self.index.0, self.symbol.n_numaux() as usize)
            {
                let smclas = aux_csect.x_smclas();
                self.symbol.n_scnum() != xcoff::N_UNDEF
                    && (smclas == xcoff::XMC_PR
                        || smclas == xcoff::XMC_RW
                        || smclas == xcoff::XMC_RO)
            } else {
                false
            }
        } else {
            false
        }
    }

    #[inline]
    fn is_common(&self) -> bool {
        self.symbol.n_sclass() == xcoff::C_EXT && self.symbol.n_scnum() == xcoff::N_UNDEF
    }

    #[inline]
    fn is_weak(&self) -> bool {
        self.symbol.n_sclass() == xcoff::C_WEAKEXT
    }

    fn scope(&self) -> SymbolScope {
        if self.symbol.n_scnum() == xcoff::N_UNDEF {
            SymbolScope::Unknown
        } else {
            match self.symbol.n_sclass() {
                xcoff::C_EXT | xcoff::C_WEAKEXT | xcoff::C_HIDEXT => {
                    let visibility = self.symbol.n_type() & xcoff::SYM_V_MASK;
                    if visibility == xcoff::SYM_V_HIDDEN {
                        SymbolScope::Linkage
                    } else {
                        SymbolScope::Dynamic
                    }
                }
                _ => SymbolScope::Compilation,
            }
        }
    }

    #[inline]
    fn is_global(&self) -> bool {
        match self.symbol.n_sclass() {
            xcoff::C_EXT | xcoff::C_WEAKEXT => true,
            _ => false,
        }
    }

    #[inline]
    fn is_local(&self) -> bool {
        !self.is_global()
    }

    #[inline]
    fn flags(&self) -> SymbolFlags<SectionIndex, SymbolIndex> {
        let mut x_smtyp = 0;
        let mut x_smclas = 0;
        let mut containing_csect = None;
        if self.symbol.has_aux_csect() {
            if let Ok(aux_csect) = self
                .file
                .symbols
                .aux_csect(self.index.0, self.symbol.n_numaux() as usize)
            {
                x_smtyp = aux_csect.x_smtyp();
                x_smclas = aux_csect.x_smclas();
                if x_smtyp == xcoff::XTY_LD {
                    containing_csect = Some(SymbolIndex(aux_csect.x_scnlen() as usize))
                }
            }
        }
        SymbolFlags::Xcoff {
            n_sclass: self.symbol.n_sclass(),
            x_smtyp,
            x_smclas,
            containing_csect,
        }
    }
}

/// A trait for generic access to `Symbol32` and `Symbol64`.
#[allow(missing_docs)]
pub trait Symbol: Debug + Pod {
    type Word: Into<u64>;

    fn n_value(&self) -> Self::Word;
    fn n_scnum(&self) -> i16;
    fn n_type(&self) -> u16;
    fn n_sclass(&self) -> u8;
    fn n_numaux(&self) -> u8;

    fn name<'data, R: ReadRef<'data>>(
        &'data self,
        strings: StringTable<'data, R>,
    ) -> Result<&'data [u8]>;

    /// Return true if the symbol is undefined.
    #[inline]
    fn is_undefined(&self) -> bool {
        let n_sclass = self.n_sclass();
        (n_sclass == xcoff::C_EXT || n_sclass == xcoff::C_WEAKEXT)
            && self.n_scnum() == xcoff::N_UNDEF
    }

    /// Return true if the symbol has file auxiliary entry.
    fn has_aux_file(&self) -> bool {
        self.n_numaux() > 0 && self.n_sclass() == xcoff::C_FILE
    }

    /// Return true if the symbol has csect auxiliary entry.
    ///
    /// A csect auxiliary entry is required for each symbol table entry that has
    /// a storage class value of C_EXT, C_WEAKEXT, or C_HIDEXT.
    fn has_aux_csect(&self) -> bool {
        let sclass = self.n_sclass();
        self.n_numaux() > 0
            && (sclass == xcoff::C_EXT || sclass == xcoff::C_WEAKEXT || sclass == xcoff::C_HIDEXT)
    }
}

impl Symbol for xcoff::Symbol64 {
    type Word = u64;

    fn n_value(&self) -> Self::Word {
        self.n_value.get(BE)
    }

    fn n_scnum(&self) -> i16 {
        self.n_scnum.get(BE)
    }

    fn n_type(&self) -> u16 {
        self.n_type.get(BE)
    }

    fn n_sclass(&self) -> u8 {
        self.n_sclass
    }

    fn n_numaux(&self) -> u8 {
        self.n_numaux
    }

    /// Parse the symbol name for XCOFF64.
    fn name<'data, R: ReadRef<'data>>(
        &'data self,
        strings: StringTable<'data, R>,
    ) -> Result<&'data [u8]> {
        strings
            .get(self.n_offset.get(BE))
            .read_error("Invalid XCOFF symbol name offset")
    }
}

impl Symbol for xcoff::Symbol32 {
    type Word = u32;

    fn n_value(&self) -> Self::Word {
        self.n_value.get(BE)
    }

    fn n_scnum(&self) -> i16 {
        self.n_scnum.get(BE)
    }

    fn n_type(&self) -> u16 {
        self.n_type.get(BE)
    }

    fn n_sclass(&self) -> u8 {
        self.n_sclass
    }

    fn n_numaux(&self) -> u8 {
        self.n_numaux
    }

    /// Parse the symbol name for XCOFF32.
    fn name<'data, R: ReadRef<'data>>(
        &'data self,
        strings: StringTable<'data, R>,
    ) -> Result<&'data [u8]> {
        if self.n_name[0] == 0 {
            // If the name starts with 0 then the last 4 bytes are a string table offset.
            let offset = u32::from_be_bytes(self.n_name[4..8].try_into().unwrap());
            strings
                .get(offset)
                .read_error("Invalid XCOFF symbol name offset")
        } else {
            // The name is inline and padded with nulls.
            Ok(match memchr::memchr(b'\0', &self.n_name) {
                Some(end) => &self.n_name[..end],
                None => &self.n_name,
            })
        }
    }
}

/// A trait for generic access to `FileAux32` and `FileAux64`.
#[allow(missing_docs)]
pub trait FileAux: Debug + Pod {
    fn x_fname(&self) -> &[u8; 8];
    fn x_ftype(&self) -> u8;
    fn x_auxtype(&self) -> Option<u8>;

    /// Parse the x_fname field, which may be an inline string or a string table offset.
    fn fname<'data, R: ReadRef<'data>>(
        &'data self,
        strings: StringTable<'data, R>,
    ) -> Result<&'data [u8]> {
        let x_fname = self.x_fname();
        if x_fname[0] == 0 {
            // If the name starts with 0 then the last 4 bytes are a string table offset.
            let offset = u32::from_be_bytes(x_fname[4..8].try_into().unwrap());
            strings
                .get(offset)
                .read_error("Invalid XCOFF symbol name offset")
        } else {
            // The name is inline and padded with nulls.
            Ok(match memchr::memchr(b'\0', x_fname) {
                Some(end) => &x_fname[..end],
                None => x_fname,
            })
        }
    }
}

impl FileAux for xcoff::FileAux64 {
    fn x_fname(&self) -> &[u8; 8] {
        &self.x_fname
    }

    fn x_ftype(&self) -> u8 {
        self.x_ftype
    }

    fn x_auxtype(&self) -> Option<u8> {
        Some(self.x_auxtype)
    }
}

impl FileAux for xcoff::FileAux32 {
    fn x_fname(&self) -> &[u8; 8] {
        &self.x_fname
    }

    fn x_ftype(&self) -> u8 {
        self.x_ftype
    }

    fn x_auxtype(&self) -> Option<u8> {
        None
    }
}

/// A trait for generic access to `CsectAux32` and `CsectAux64`.
#[allow(missing_docs)]
pub trait CsectAux: Debug + Pod {
    fn x_scnlen(&self) -> u64;
    fn x_parmhash(&self) -> u32;
    fn x_snhash(&self) -> u16;
    fn x_smtyp(&self) -> u8;
    fn x_smclas(&self) -> u8;
    fn x_auxtype(&self) -> Option<u8>;

    fn sym_type(&self) -> u8 {
        self.x_smtyp() & 0x07
    }
}

impl CsectAux for xcoff::CsectAux64 {
    fn x_scnlen(&self) -> u64 {
        self.x_scnlen_lo.get(BE) as u64 | ((self.x_scnlen_hi.get(BE) as u64) << 32)
    }

    fn x_parmhash(&self) -> u32 {
        self.x_parmhash.get(BE)
    }

    fn x_snhash(&self) -> u16 {
        self.x_snhash.get(BE)
    }

    fn x_smtyp(&self) -> u8 {
        self.x_smtyp
    }

    fn x_smclas(&self) -> u8 {
        self.x_smclas
    }

    fn x_auxtype(&self) -> Option<u8> {
        Some(self.x_auxtype)
    }
}

impl CsectAux for xcoff::CsectAux32 {
    fn x_scnlen(&self) -> u64 {
        self.x_scnlen.get(BE) as u64
    }

    fn x_parmhash(&self) -> u32 {
        self.x_parmhash.get(BE)
    }

    fn x_snhash(&self) -> u16 {
        self.x_snhash.get(BE)
    }

    fn x_smtyp(&self) -> u8 {
        self.x_smtyp
    }

    fn x_smclas(&self) -> u8 {
        self.x_smclas
    }

    fn x_auxtype(&self) -> Option<u8> {
        None
    }
}
