//! Backtrace strategy for Windows platforms.
//!
//! This module contains the ability to generate a backtrace on Windows using one
//! of two possible methods. The `StackWalkEx` function is primarily used if
//! possible, but not all systems have that. Failing that the `StackWalk64`
//! function is used instead. Note that `StackWalkEx` is favored because it
//! handles debuginfo internally and returns inline frame information.
//!
//! Note that all dbghelp support is loaded dynamically, see `src/dbghelp.rs`
//! for more information about that.
#![deny(unsafe_op_in_unsafe_fn)]

use super::super::{dbghelp, windows_sys::*};
use core::ffi::c_void;
use core::mem;

#[derive(Clone, Copy)]
pub enum StackFrame {
    New(STACKFRAME_EX),
    Old(STACKFRAME64),
}

#[derive(Clone, Copy)]
pub struct Frame {
    pub(crate) stack_frame: StackFrame,
    base_address: *mut c_void,
}

// we're just sending around raw pointers and reading them, never interpreting
// them so this should be safe to both send and share across threads.
unsafe impl Send for Frame {}
unsafe impl Sync for Frame {}

impl Frame {
    pub fn ip(&self) -> *mut c_void {
        self.addr_pc().Offset as *mut _
    }

    pub fn sp(&self) -> *mut c_void {
        self.addr_stack().Offset as *mut _
    }

    pub fn symbol_address(&self) -> *mut c_void {
        self.ip()
    }

    pub fn module_base_address(&self) -> Option<*mut c_void> {
        Some(self.base_address)
    }

    #[cfg(not(target_env = "gnu"))]
    pub fn inline_context(&self) -> Option<u32> {
        match self.stack_frame {
            StackFrame::New(ref new) => Some(new.InlineFrameContext),
            StackFrame::Old(_) => None,
        }
    }

    fn addr_pc(&self) -> &ADDRESS64 {
        match self.stack_frame {
            StackFrame::New(ref new) => &new.AddrPC,
            StackFrame::Old(ref old) => &old.AddrPC,
        }
    }

    fn addr_pc_mut(&mut self) -> &mut ADDRESS64 {
        match self.stack_frame {
            StackFrame::New(ref mut new) => &mut new.AddrPC,
            StackFrame::Old(ref mut old) => &mut old.AddrPC,
        }
    }

    fn addr_frame_mut(&mut self) -> &mut ADDRESS64 {
        match self.stack_frame {
            StackFrame::New(ref mut new) => &mut new.AddrFrame,
            StackFrame::Old(ref mut old) => &mut old.AddrFrame,
        }
    }

    fn addr_stack(&self) -> &ADDRESS64 {
        match self.stack_frame {
            StackFrame::New(ref new) => &new.AddrStack,
            StackFrame::Old(ref old) => &old.AddrStack,
        }
    }

    fn addr_stack_mut(&mut self) -> &mut ADDRESS64 {
        match self.stack_frame {
            StackFrame::New(ref mut new) => &mut new.AddrStack,
            StackFrame::Old(ref mut old) => &mut old.AddrStack,
        }
    }
}

#[repr(C, align(16))] // required by `CONTEXT`, is a FIXME in windows metadata right now
struct MyContext(CONTEXT);

#[inline(always)]
pub unsafe fn trace(cb: &mut dyn FnMut(&super::Frame) -> bool) {
    // Allocate necessary structures for doing the stack walk
    let process = unsafe { GetCurrentProcess() };
    let thread = unsafe { GetCurrentThread() };

    // This is a classic C-style out-ptr struct. Zero it to start.
    let mut context = unsafe { mem::zeroed::<MyContext>() };
    unsafe { RtlCaptureContext(&mut context.0) };

    // Ensure this process's symbols are initialized
    let dbghelp = match dbghelp::init() {
        Ok(dbghelp) => dbghelp,
        Err(()) => return, // oh well...
    };

    let function_table_access = dbghelp.SymFunctionTableAccess64();
    let get_module_base = dbghelp.SymGetModuleBase64();

    // Attempt to use `StackWalkEx` if we can, but fall back to `StackWalk64`
    // since it's in theory supported on more systems.
    match unsafe { (*dbghelp.dbghelp()).StackWalkEx() } {
        #[allow(non_snake_case)]
        Some(StackWalkEx) => {
            // This is a classic C-style out-ptr struct. Zero it to start.
            let mut inner: STACKFRAME_EX = unsafe { mem::zeroed() };
            inner.StackFrameSize = mem::size_of::<STACKFRAME_EX>() as u32;
            let mut frame = super::Frame {
                inner: Frame {
                    stack_frame: StackFrame::New(inner),
                    base_address: 0 as _,
                },
            };
            let image = init_frame(&mut frame.inner, &context.0);
            let frame_ptr = match &mut frame.inner.stack_frame {
                StackFrame::New(ptr) => ptr as *mut STACKFRAME_EX,
                _ => unreachable!(),
            };

            while unsafe {
                StackWalkEx(
                    image as u32,
                    process,
                    thread,
                    frame_ptr,
                    &mut context.0 as *mut CONTEXT as *mut _,
                    None,
                    Some(function_table_access),
                    Some(get_module_base),
                    None,
                    0,
                ) == TRUE
            } {
                frame.inner.base_address =
                    unsafe { get_module_base(process, frame.ip() as _) as _ };

                if !cb(&frame) {
                    break;
                }
            }
        }
        None => {
            let mut frame = super::Frame {
                inner: Frame {
                    // This is a classic C-style out-ptr struct. Zero it to start.
                    stack_frame: StackFrame::Old(unsafe { mem::zeroed() }),
                    base_address: 0 as _,
                },
            };
            let image = init_frame(&mut frame.inner, &context.0);
            let frame_ptr = match &mut frame.inner.stack_frame {
                StackFrame::Old(ptr) => ptr as *mut STACKFRAME64,
                _ => unreachable!(),
            };

            while unsafe {
                dbghelp.StackWalk64()(
                    image as u32,
                    process,
                    thread,
                    frame_ptr,
                    &mut context.0 as *mut CONTEXT as *mut _,
                    None,
                    Some(function_table_access),
                    Some(get_module_base),
                    None,
                ) == TRUE
            } {
                frame.inner.base_address =
                    unsafe { get_module_base(process, frame.ip() as _) as _ };

                if !cb(&frame) {
                    break;
                }
            }
        }
    }
}

#[cfg(target_arch = "x86")]
fn init_frame(frame: &mut Frame, ctx: &CONTEXT) -> u16 {
    frame.addr_pc_mut().Offset = ctx.Eip as u64;
    frame.addr_pc_mut().Mode = AddrModeFlat;
    frame.addr_stack_mut().Offset = ctx.Esp as u64;
    frame.addr_stack_mut().Mode = AddrModeFlat;
    frame.addr_frame_mut().Offset = ctx.Ebp as u64;
    frame.addr_frame_mut().Mode = AddrModeFlat;

    IMAGE_FILE_MACHINE_I386
}

#[cfg(target_arch = "arm")]
fn init_frame(frame: &mut Frame, ctx: &CONTEXT) -> u16 {
    frame.addr_pc_mut().Offset = ctx.Pc as u64;
    frame.addr_pc_mut().Mode = AddrModeFlat;
    frame.addr_stack_mut().Offset = ctx.Sp as u64;
    frame.addr_stack_mut().Mode = AddrModeFlat;
    unsafe {
        frame.addr_frame_mut().Offset = ctx.R11 as u64;
    }
    frame.addr_frame_mut().Mode = AddrModeFlat;
    IMAGE_FILE_MACHINE_ARMNT
}
