use super::*;

use mach2::exception_types as et;

impl MinidumpWriter {
    /// Writes the [`minidump_common::format::MINIDUMP_EXCEPTION_STREAM`] stream.
    ///
    /// This stream is optional on MacOS as a user requested minidump could
    /// choose not to specify the exception information.
    pub(crate) fn write_exception(
        &mut self,
        buffer: &mut DumpBuf,
        dumper: &TaskDumper,
    ) -> Result<MDRawDirectory, WriterError> {
        // This shouldn't fail since we won't be writing this stream if the crash context is
        // not present
        let crash_context = self
            .crash_context
            .as_ref()
            .ok_or(WriterError::NoCrashContext)?;

        let thread_state = dumper.read_thread_state(crash_context.thread).ok();

        let thread_context = if let Some(ts) = &thread_state {
            let mut cpu = Default::default();
            Self::fill_cpu_context(ts, &mut cpu);
            MemoryWriter::alloc_with_val(buffer, cpu)
                .map(|mw| mw.location())
                .ok()
        } else {
            None
        };

        let exception_record = crash_context
            .exception
            .as_ref()
            .map(|exc| {
                let code = exc.code as u64;

                // `EXC_CRASH` exceptions wrap other exceptions, so we want to
                // retrieve the _actual_ exception
                let wrapped_exc = if exc.kind as u32 == et::EXC_CRASH {
                    recover_exc_crash_wrapped_exception(code)
                } else {
                    None
                };

                // For EXC_RESOURCE and EXC_GUARD crashes Crashpad records the
                // uppermost 32 bits of the exception code in the exception flags,
                // as they are the most interesting for those exceptions. Neither
                // of these exceptions can be wrapped by an `EXC_CRASH`
                //
                // EXC_GUARD
                // code:
                // +-------------------+----------------+--------------+
                // |[63:61] guard type | [60:32] flavor | [31:0] target|
                // +-------------------+----------------+--------------+
                //
                // EXC_RESOURCE
                // code:
                // +--------------------------------------------------------+
                // |[63:61] resource type | [60:58] flavor | [57:32] unused |
                // +--------------------------------------------------------+
                let exception_code =
                    if exc.kind as u32 == et::EXC_RESOURCE || exc.kind as u32 == et::EXC_GUARD {
                        (code >> 32) as u32
                    } else if let Some(wrapped) = wrapped_exc {
                        wrapped.code
                    } else {
                        // For all other exceptions types, the value in the code
                        // _should_ never exceed 32 bits, crashpad does an actual
                        // range check here, but since we don't really log anything
                        // else at the moment I'll punt that for now
                        // TODO: log/do something if exc.code > u32::MAX
                        code as u32
                    };

                let exception_kind = if let Some(wrapped) = wrapped_exc {
                    wrapped.kind
                } else {
                    exc.kind
                };

                let exception_address =
                    if exception_kind == et::EXC_BAD_ACCESS && exc.subcode.is_some() {
                        exc.subcode.unwrap_or_default()
                    } else if let Some(ts) = thread_state {
                        ts.pc()
                    } else {
                        0
                    };

                // The naming is confusing here, but it is how it is
                let mut md_exc = MDException {
                    exception_code: exception_kind,
                    exception_flags: exception_code,
                    exception_address,
                    ..Default::default()
                };

                // Now append the (mostly) original information to the "ancillary"
                // exception_information at the end. This allows a minidump parser
                // to recover the full exception information for the crash, rather
                // than only using the (potentially) truncated information we
                // just set in `exception_code` and `exception_flags`
                md_exc.exception_information[0] = exception_kind as u64;
                md_exc.exception_information[1] = code;

                md_exc.number_parameters = if let Some(subcode) = exc.subcode {
                    md_exc.exception_information[2] = subcode;
                    3
                } else {
                    2
                };

                md_exc
            })
            .unwrap_or_default();

        let stream = MDRawExceptionStream {
            thread_id: crash_context.thread,
            exception_record,
            thread_context: thread_context.unwrap_or_default(),
            __align: 0,
        };

        let exc_section = MemoryWriter::<MDRawExceptionStream>::alloc_with_val(buffer, stream)?;

        Ok(MDRawDirectory {
            stream_type: MDStreamType::ExceptionStream as u32,
            location: exc_section.location(),
        })
    }
}

/// [`et::EXC_CRASH`] is a wrapper exception around another exception, but not
/// all exceptions can be wrapped by it, so this function validates that the
/// `EXC_CRASH` is actually valid
#[inline]
fn is_valid_exc_crash(exc_code: u64) -> bool {
    let wrapped = ((exc_code >> 20) & 0xf) as u32;

    !(
        wrapped == et::EXC_CRASH // EXC_CRASH can't wrap another one
        || wrapped == et::EXC_RESOURCE // EXC_RESOURCE would lose information
        || wrapped == et::EXC_GUARD // EXC_GUARD would lose information
        || wrapped == et::EXC_CORPSE_NOTIFY
        // cannot be wrapped
    )
}

/// The details for an exception wrapped by an `EXC_CRASH`
#[derive(Copy, Clone)]
struct WrappedException {
    /// The `EXC_*` that was wrapped
    kind: u32,
    /// The code of the wrapped exception, for all exceptions other than
    /// `EXC_RESOURCE` and `EXC_GUARD` this _should_ never exceed 32 bits, and
    /// is one of the reasons that `EXC_CRASH` cannot wrap those 2 exceptions
    code: u32,
    /// The Unix signal number that the original exception was converted into
    _signal: u8,
}

/// Unwraps an `EXC_CRASH` exception code to the inner exception it wraps.
///
/// Will return `None` if the specified code is wrapping an exception that
/// should not be possible to be wrapped in an `EXC_CRASH`
#[inline]
fn recover_exc_crash_wrapped_exception(code: u64) -> Option<WrappedException> {
    is_valid_exc_crash(code).then(|| WrappedException {
        kind: ((code >> 20) & 0xf) as u32,
        code: (code & 0xfffff) as u32,
        _signal: ((code >> 24) & 0xff) as u8,
    })
}
