use std::error::Error as StdError;
use std::fmt;

pub(crate) type Result<T> = std::result::Result<T, Error>;

/// Helper trait for converting an error to a `&dyn std::error::Error`.
pub trait AsDynError<'a> {
    fn as_dyn_error(&self) -> &(dyn StdError + 'a);
}

impl<'a, T: StdError + 'a> AsDynError<'a> for T {
    #[inline]
    fn as_dyn_error(&self) -> &(dyn StdError + 'a) {
        self
    }
}

/// Diagnostics (and contexts) emitted during DWARF packaging.
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
    /// Failure to read input file.
    ///
    /// This error occurs in the `Session::read_input` function provided by the user of `thorin`.
    ReadInput(std::io::Error),
    /// Failed to parse kind of input file.
    ///
    /// Input file kind is necessary to determine how to parse the rest of the input, and to
    /// validate that the input file is of a type that `thorin` can process.
    ParseFileKind(object::Error),
    /// Failed to parse object file.
    ParseObjectFile(object::Error),
    /// Failed to parse archive file.
    ParseArchiveFile(object::Error),
    /// Failed to parse archive member.
    ParseArchiveMember(object::Error),
    /// Invalid kind of input.
    ///
    /// Only archive and elf files are supported input files.
    InvalidInputKind,
    /// Failed to decompress data.
    ///
    /// `thorin` uses `object` for decompression, so `object` probably didn't have support for the
    /// type of compression used.
    DecompressData(object::Error),
    /// Section without a name.
    NamelessSection(object::Error, usize),
    /// Relocation has invalid symbol for a section.
    RelocationWithInvalidSymbol(String, usize),
    /// Multiple relocations for a section.
    MultipleRelocations(String, usize),
    /// Unsupported relocations for a section.
    UnsupportedRelocation(String, usize),
    /// Input object that has a `DwoId` (or `DebugTypeSignature`) does not have a
    /// `DW_AT_GNU_dwo_name` or `DW_AT_dwo_name` attribute.
    MissingDwoName(u64),
    /// Input object has no compilation units.
    NoCompilationUnits,
    /// No top-level debugging information entry in unit.
    NoDie,
    /// Top-level debugging information entry is not a compilation/type unit.
    TopLevelDieNotUnit,
    /// Section required of input DWARF objects was missing.
    MissingRequiredSection(&'static str),
    /// Failed to parse unit abbreviations.
    ParseUnitAbbreviations(gimli::read::Error),
    /// Failed to parse unit attribute.
    ParseUnitAttribute(gimli::read::Error),
    /// Failed to parse unit header.
    ParseUnitHeader(gimli::read::Error),
    /// Failed to parse unit.
    ParseUnit(gimli::read::Error),
    /// Input DWARF package has a different index version than the version being output.
    IncompatibleIndexVersion(String, u16, u16),
    /// Failed to read string offset from `.debug_str_offsets` at index.
    OffsetAtIndex(gimli::read::Error, u64),
    /// Failed to read string from `.debug_str` at offset.
    StrAtOffset(gimli::read::Error, usize),
    /// Failed to parse index section.
    ///
    /// If an input file is a DWARF package, its index section needs to be read to ensure that the
    /// contributions within it are preserved.
    ParseIndex(gimli::read::Error, String),
    /// Compilation unit in DWARF package is not its index.
    UnitNotInIndex(u64),
    /// Row for a compilation unit is not in the index.
    RowNotInIndex(gimli::read::Error, u32),
    /// Section not found in unit's row in index, i.e. a DWARF package contains a section but its
    /// index doesn't record contributions to it.
    SectionNotInRow,
    /// Compilation unit in input DWARF object has no content.
    EmptyUnit(u64),
    /// Found multiple `.debug_info.dwo` sections.
    MultipleDebugInfoSection,
    /// Found multiple `.debug_types.dwo` sections in a DWARF package file.
    MultipleDebugTypesSection,
    /// Found a regular compilation unit in a DWARF object.
    NotSplitUnit,
    /// Found duplicate split compilation unit.
    DuplicateUnit(u64),
    /// Unit referenced by an executable was not found.
    MissingReferencedUnit(u64),
    /// No output object was created from inputs
    NoOutputObjectCreated,
    /// Input objects have different encodings.
    MixedInputEncodings,

    /// Catch-all for `std::io::Error`.
    Io(std::io::Error),
    /// Catch-all for `object::Error`.
    ObjectRead(object::Error),
    /// Catch-all for `object::write::Error`.
    ObjectWrite(object::write::Error),
    /// Catch-all for `gimli::read::Error`.
    GimliRead(gimli::read::Error),
    /// Catch-all for `gimli::write::Error`.
    GimliWrite(gimli::write::Error),
}

impl StdError for Error {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        match self {
            Error::ReadInput(source) => Some(source.as_dyn_error()),
            Error::ParseFileKind(source) => Some(source.as_dyn_error()),
            Error::ParseObjectFile(source) => Some(source.as_dyn_error()),
            Error::ParseArchiveFile(source) => Some(source.as_dyn_error()),
            Error::ParseArchiveMember(source) => Some(source.as_dyn_error()),
            Error::InvalidInputKind => None,
            Error::DecompressData(source) => Some(source.as_dyn_error()),
            Error::NamelessSection(source, _) => Some(source.as_dyn_error()),
            Error::RelocationWithInvalidSymbol(_, _) => None,
            Error::MultipleRelocations(_, _) => None,
            Error::UnsupportedRelocation(_, _) => None,
            Error::MissingDwoName(_) => None,
            Error::NoCompilationUnits => None,
            Error::NoDie => None,
            Error::TopLevelDieNotUnit => None,
            Error::MissingRequiredSection(_) => None,
            Error::ParseUnitAbbreviations(source) => Some(source.as_dyn_error()),
            Error::ParseUnitAttribute(source) => Some(source.as_dyn_error()),
            Error::ParseUnitHeader(source) => Some(source.as_dyn_error()),
            Error::ParseUnit(source) => Some(source.as_dyn_error()),
            Error::IncompatibleIndexVersion(_, _, _) => None,
            Error::OffsetAtIndex(source, _) => Some(source.as_dyn_error()),
            Error::StrAtOffset(source, _) => Some(source.as_dyn_error()),
            Error::ParseIndex(source, _) => Some(source.as_dyn_error()),
            Error::UnitNotInIndex(_) => None,
            Error::RowNotInIndex(source, _) => Some(source.as_dyn_error()),
            Error::SectionNotInRow => None,
            Error::EmptyUnit(_) => None,
            Error::MultipleDebugInfoSection => None,
            Error::MultipleDebugTypesSection => None,
            Error::NotSplitUnit => None,
            Error::DuplicateUnit(_) => None,
            Error::MissingReferencedUnit(_) => None,
            Error::NoOutputObjectCreated => None,
            Error::MixedInputEncodings => None,
            Error::Io(transparent) => StdError::source(transparent.as_dyn_error()),
            Error::ObjectRead(transparent) => StdError::source(transparent.as_dyn_error()),
            Error::ObjectWrite(transparent) => StdError::source(transparent.as_dyn_error()),
            Error::GimliRead(transparent) => StdError::source(transparent.as_dyn_error()),
            Error::GimliWrite(transparent) => StdError::source(transparent.as_dyn_error()),
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::ReadInput(_) => write!(f, "Failed to read input file"),
            Error::ParseFileKind(_) => write!(f, "Failed to parse input file kind"),
            Error::ParseObjectFile(_) => write!(f, "Failed to parse input object file"),
            Error::ParseArchiveFile(_) => write!(f, "Failed to parse input archive file"),
            Error::ParseArchiveMember(_) => write!(f, "Failed to parse archive member"),
            Error::InvalidInputKind => write!(f, "Input is not an archive or elf object"),
            Error::DecompressData(_) => write!(f, "Failed to decompress compressed section"),
            Error::NamelessSection(_, offset) => {
                write!(f, "Section without name at offset 0x{:08x}", offset)
            }
            Error::RelocationWithInvalidSymbol(section, offset) => write!(
                f,
                "Relocation with invalid symbol for section `{}` at offset 0x{:08x}",
                section, offset
            ),
            Error::MultipleRelocations(section, offset) => write!(
                f,
                "Multiple relocations for section `{}` at offset 0x{:08x}",
                section, offset
            ),
            Error::UnsupportedRelocation(section, offset) => write!(
                f,
                "Unsupported relocation for section {} at offset 0x{:08x}",
                section, offset
            ),
            Error::MissingDwoName(id) => {
                write!(f, "Missing path attribute to DWARF object (0x{:08x})", id)
            }
            Error::NoCompilationUnits => {
                write!(f, "Input object has no compilation units")
            }
            Error::NoDie => {
                write!(f, "No top-level debugging information entry in compilation/type unit")
            }
            Error::TopLevelDieNotUnit => {
                write!(f, "Top-level debugging information entry is not a compilation/type unit")
            }
            Error::MissingRequiredSection(section) => {
                write!(f, "Input object missing required section `{}`", section)
            }
            Error::ParseUnitAbbreviations(_) => write!(f, "Failed to parse unit abbreviations"),
            Error::ParseUnitAttribute(_) => write!(f, "Failed to parse unit attribute"),
            Error::ParseUnitHeader(_) => write!(f, "Failed to parse unit header"),
            Error::ParseUnit(_) => write!(f, "Failed to parse unit"),
            Error::IncompatibleIndexVersion(section, format, actual) => {
                write!(
                    f,
                    "Incompatible `{}` index version: found version {}, expected version {}",
                    section, actual, format
                )
            }
            Error::OffsetAtIndex(_, index) => {
                write!(f, "Read offset at index {} of `.debug_str_offsets.dwo` section", index)
            }
            Error::StrAtOffset(_, offset) => {
                write!(f, "Read string at offset 0x{:08x} of `.debug_str.dwo` section", offset)
            }
            Error::ParseIndex(_, section) => {
                write!(f, "Failed to parse `{}` index section", section)
            }
            Error::UnitNotInIndex(unit) => {
                write!(f, "Unit 0x{0:08x} from input package is not in its index", unit)
            }
            Error::RowNotInIndex(_, row) => {
                write!(f, "Row {0} found in index's hash table not present in index", row)
            }
            Error::SectionNotInRow => write!(f, "Section not found in unit's row in index"),
            Error::EmptyUnit(unit) => {
                write!(f, "Unit 0x{:08x} in input DWARF object with no data", unit)
            }
            Error::MultipleDebugInfoSection => {
                write!(f, "Multiple `.debug_info.dwo` sections")
            }
            Error::MultipleDebugTypesSection => {
                write!(f, "Multiple `.debug_types.dwo` sections in a package")
            }
            Error::NotSplitUnit => {
                write!(f, "Regular compilation unit in object (missing dwo identifier)")
            }
            Error::DuplicateUnit(unit) => {
                write!(f, "Duplicate split compilation unit (0x{:08x})", unit)
            }
            Error::MissingReferencedUnit(unit) => {
                write!(f, "Unit 0x{:08x} referenced by executable was not found", unit)
            }
            Error::NoOutputObjectCreated => write!(f, "No output object was created from inputs"),
            Error::MixedInputEncodings => write!(f, "Input objects haved mixed encodings"),
            Error::Io(e) => fmt::Display::fmt(e, f),
            Error::ObjectRead(e) => fmt::Display::fmt(e, f),
            Error::ObjectWrite(e) => fmt::Display::fmt(e, f),
            Error::GimliRead(e) => fmt::Display::fmt(e, f),
            Error::GimliWrite(e) => fmt::Display::fmt(e, f),
        }
    }
}

impl From<std::io::Error> for Error {
    fn from(source: std::io::Error) -> Self {
        Error::Io(source)
    }
}

impl From<object::Error> for Error {
    fn from(source: object::Error) -> Self {
        Error::ObjectRead(source)
    }
}

impl From<object::write::Error> for Error {
    fn from(source: object::write::Error) -> Self {
        Error::ObjectWrite(source)
    }
}

impl From<gimli::read::Error> for Error {
    fn from(source: gimli::read::Error) -> Self {
        Error::GimliRead(source)
    }
}

impl From<gimli::write::Error> for Error {
    fn from(source: gimli::write::Error) -> Self {
        Error::GimliWrite(source)
    }
}