use core::{arch::asm, fmt::Display};

use crate::MAX_BACKTRACE_ADDRESSES;

// subtract 3 from the return address
// the return address is the address following the callxN
// we get better results (especially if the caller was the last function in the
// calling function) if we report the address of callxN itself
#[allow(unused)]
pub(super) const RA_OFFSET: usize = 3;

/// Exception Cause
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(C)]
pub enum ExceptionCause {
    /// Illegal Instruction
    IllegalInstruction             = 0,
    /// System Call (Syscall Instruction)
    Syscall                        = 1,
    /// Instruction Fetch Error
    InstrFetchError                = 2,
    /// Load Store Error
    LoadStoreError                 = 3,
    /// Level 1 Interrupt
    LevelOneInterrupt              = 4,
    /// Stack Extension Assist (movsp Instruction) For Alloca
    Alloca                         = 5,
    /// Integer Divide By Zero
    DivideByZero                   = 6,
    /// Use Of Failed Speculative Access (Not Implemented)
    NextPCValueIllegal             = 7,
    /// Privileged Instruction
    PrivilegedInstruction          = 8,
    /// Unaligned Load Or Store
    UnalignedLoadOrStore           = 9,
    /// Reserved
    ExternalRegisterPrivilegeError = 10,
    /// Reserved
    ExclusiveError                 = 11,
    /// Pif Data Error On Instruction Fetch (Rb-200x And Later)
    InstrDataError                 = 12,
    /// Pif Data Error On Load Or Store (Rb-200x And Later)
    LoadStoreDataError             = 13,
    /// Pif Address Error On Instruction Fetch (Rb-200x And Later)
    InstrAddrError                 = 14,
    /// Pif Address Error On Load Or Store (Rb-200x And Later)
    LoadStoreAddrError             = 15,
    /// Itlb Miss (No Itlb Entry Matches, Hw Refill Also Missed)
    ItlbMiss                       = 16,
    /// Itlb Multihit (Multiple Itlb Entries Match)
    ItlbMultiHit                   = 17,
    /// Ring Privilege Violation On Instruction Fetch
    InstrRing                      = 18,
    /// Size Restriction On Ifetch (Not Implemented)
    Reserved19                     = 19,
    /// Cache Attribute Does Not Allow Instruction Fetch
    InstrProhibited                = 20,
    /// Reserved
    Reserved21                     = 21,
    /// Reserved
    Reserved22                     = 22,
    /// Reserved
    Reserved23                     = 23,
    /// Dtlb Miss (No Dtlb Entry Matches, Hw Refill Also Missed)
    DtlbMiss                       = 24,
    /// Dtlb Multihit (Multiple Dtlb Entries Match)
    DtlbMultiHit                   = 25,
    /// Ring Privilege Violation On Load Or Store
    LoadStoreRing                  = 26,
    /// Size Restriction On Load/Store (Not Implemented)
    Reserved27                     = 27,
    /// Cache Attribute Does Not Allow Load
    LoadProhibited                 = 28,
    /// Cache Attribute Does Not Allow Store
    StoreProhibited                = 29,
    /// Reserved
    Reserved30                     = 30,
    /// Reserved
    Reserved31                     = 31,
    /// Access To Coprocessor 0 When Disabled
    Cp0Disabled                    = 32,
    /// Access To Coprocessor 1 When Disabled
    Cp1Disabled                    = 33,
    /// Access To Coprocessor 2 When Disabled
    Cp2Disabled                    = 34,
    /// Access To Coprocessor 3 When Disabled
    Cp3Disabled                    = 35,
    /// Access To Coprocessor 4 When Disabled
    Cp4Disabled                    = 36,
    /// Access To Coprocessor 5 When Disabled
    Cp5Disabled                    = 37,
    /// Access To Coprocessor 6 When Disabled
    Cp6Disabled                    = 38,
    /// Access To Coprocessor 7 When Disabled
    Cp7Disabled                    = 39,
    /// None
    None                           = 255,
}

impl Display for ExceptionCause {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        if *self == Self::Cp0Disabled {
            write!(f, "Cp0Disabled (Access to the floating point coprocessor is not allowed. You may want to enable the `float-save-restore` feature of the `xtensa-lx-rt` crate.)")
        } else {
            write!(f, "{:?}", self)
        }
    }
}

#[doc(hidden)]
#[allow(non_snake_case)]
#[derive(Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(C)]
pub struct Context {
    /// Program counter, stores the address of the next instruction to be
    /// executed.
    pub PC: u32,
    /// Processor status, holds various status flags for the CPU.
    pub PS: u32,
    /// General-purpose register A0 used for data storage and manipulation.
    pub A0: u32,
    /// General-purpose register A1 used for data storage and manipulation.
    pub A1: u32,
    /// General-purpose register A2 used for data storage and manipulation.
    pub A2: u32,
    /// General-purpose register A3 used for data storage and manipulation.
    pub A3: u32,
    /// General-purpose register A4 used for data storage and manipulation.
    pub A4: u32,
    /// General-purpose register A5 used for data storage and manipulation.
    pub A5: u32,
    /// General-purpose register A6 used for data storage and manipulation.
    pub A6: u32,
    /// General-purpose register A7 used for data storage and manipulation.
    pub A7: u32,
    /// General-purpose register A8 used for data storage and manipulation.
    pub A8: u32,
    /// General-purpose register A9 used for data storage and manipulation.
    pub A9: u32,
    /// General-purpose register A10 used for data storage and manipulation.
    pub A10: u32,
    /// General-purpose register A11 used for data storage and manipulation.
    pub A11: u32,
    /// General-purpose register A12 used for data storage and manipulation.
    pub A12: u32,
    /// General-purpose register A13 used for data storage and manipulation.
    pub A13: u32,
    /// General-purpose register A14 used for data storage and manipulation.
    pub A14: u32,
    /// General-purpose register A15 used for data storage and manipulation.
    pub A15: u32,
    /// Shift amount register, used for shift and rotate instructions.
    pub SAR: u32,
    /// Exception cause, indicates the reason for the last exception.
    pub EXCCAUSE: u32,
    /// Exception address, holds the address related to the exception.
    pub EXCVADDR: u32,
    /// Loop start address, used in loop instructions.
    pub LBEG: u32,
    /// Loop end address, used in loop instructions.
    pub LEND: u32,
    /// Loop counter, used to count iterations in loop instructions.
    pub LCOUNT: u32,
    /// Thread pointer, used for thread-local storage.
    pub THREADPTR: u32,
    /// Compare register, used for certain compare instructions.
    pub SCOMPARE1: u32,
    /// Break register, used for breakpoint-related operations.
    pub BR: u32,
    /// Accumulator low register, used for extended arithmetic operations.
    pub ACCLO: u32,
    /// Accumulator high register, used for extended arithmetic operations.
    pub ACCHI: u32,
    /// Additional register M0 used for special operations.
    pub M0: u32,
    /// Additional register M1 used for special operations.
    pub M1: u32,
    /// Additional register M2 used for special operations.
    pub M2: u32,
    /// Additional register M3 used for special operations.
    pub M3: u32,
    /// 64-bit floating-point register (low part), available if the
    /// `print-float-registers` feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F64R_LO: u32,
    /// 64-bit floating-point register (high part), available if the
    /// `print-float-registers` feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F64R_HI: u32,
    /// Floating-point status register, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F64S: u32,
    /// Floating-point control register, available if the
    /// `print-float-registers` feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub FCR: u32,
    /// Floating-point status register, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub FSR: u32,
    /// Floating-point register F0, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F0: u32,
    /// Floating-point register F1, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F1: u32,
    /// Floating-point register F2, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F2: u32,
    /// Floating-point register F3, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F3: u32,
    /// Floating-point register F4, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F4: u32,
    /// Floating-point register F5, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F5: u32,
    /// Floating-point register F6, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F6: u32,
    /// Floating-point register F7, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F7: u32,
    /// Floating-point register F8, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F8: u32,
    /// Floating-point register F9, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F9: u32,
    /// Floating-point register F10, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F10: u32,
    /// Floating-point register F11, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F11: u32,
    /// Floating-point register F12, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F12: u32,
    /// Floating-point register F13, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F13: u32,
    /// Floating-point register F14, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F14: u32,
    /// Floating-point register F15, available if the `print-float-registers`
    /// feature is enabled.
    #[cfg(feature = "print-float-registers")]
    pub F15: u32,
}

impl core::fmt::Debug for Context {
    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
        write!(
            fmt,
            "Context
PC=0x{:08x}       PS=0x{:08x}
A0=0x{:08x}       A1=0x{:08x}       A2=0x{:08x}       A3=0x{:08x}       A4=0x{:08x}
A5=0x{:08x}       A6=0x{:08x}       A7=0x{:08x}       A8=0x{:08x}       A9=0x{:08x}
A10=0x{:08x}      A11=0x{:08x}      A12=0x{:08x}      A13=0x{:08x}      A14=0x{:08x}
A15=0x{:08x}
SAR={:08x}
EXCCAUSE=0x{:08x} EXCVADDR=0x{:08x}
LBEG=0x{:08x}     LEND=0x{:08x}     LCOUNT=0x{:08x}
THREADPTR=0x{:08x}
SCOMPARE1=0x{:08x}
BR=0x{:08x}
ACCLO=0x{:08x}    ACCHI=0x{:08x}
M0=0x{:08x}       M1=0x{:08x}       M2=0x{:08x}       M3=0x{:08x}
",
            self.PC,
            self.PS,
            self.A0,
            self.A1,
            self.A2,
            self.A3,
            self.A4,
            self.A5,
            self.A6,
            self.A7,
            self.A8,
            self.A9,
            self.A10,
            self.A11,
            self.A12,
            self.A13,
            self.A14,
            self.A15,
            self.SAR,
            self.EXCCAUSE,
            self.EXCVADDR,
            self.LBEG,
            self.LEND,
            self.LCOUNT,
            self.THREADPTR,
            self.SCOMPARE1,
            self.BR,
            self.ACCLO,
            self.ACCHI,
            self.M0,
            self.M1,
            self.M2,
            self.M3,
        )?;
        #[cfg(feature = "print-float-registers")]
        write!(
            fmt,
            "F64R_LO=0x{:08x}  F64R_HI=0x{:08x}  F64S=0x{:08x}
FCR=0x{:08x}      FSR=0x{:08x}
F0=0x{:08x}       F1=0x{:08x}       F2=0x{:08x}       F3=0x{:08x}       F4=0x{:08x}
F5=0x{:08x}       F6=0x{:08x}       F7=0x{:08x}       F8=0x{:08x}       F9=0x{:08x}
F10=0x{:08x}      F11=0x{:08x}      F12=0x{:08x}      F13=0x{:08x}      F14=0x{:08x}
F15=0x{:08x}",
            self.F64R_LO,
            self.F64R_HI,
            self.F64S,
            self.FCR,
            self.FSR,
            self.F0,
            self.F1,
            self.F2,
            self.F3,
            self.F4,
            self.F5,
            self.F6,
            self.F7,
            self.F8,
            self.F9,
            self.F10,
            self.F11,
            self.F12,
            self.F13,
            self.F14,
            self.F15,
        )?;

        Ok(())
    }
}

/// Get an array of backtrace addresses.
pub fn backtrace() -> [Option<usize>; MAX_BACKTRACE_ADDRESSES] {
    let sp = unsafe {
        let mut _tmp: u32;
        asm!("mov {0}, a1", out(reg) _tmp);
        _tmp
    };

    backtrace_internal(sp, 1)
}

pub(crate) fn sanitize_address(address: u32) -> u32 {
    (address & 0x3fff_ffff) | 0x4000_0000
}

pub(crate) fn backtrace_internal(
    sp: u32,
    suppress: i32,
) -> [Option<usize>; MAX_BACKTRACE_ADDRESSES] {
    let mut result = [None; 10];
    let mut index = 0;

    let mut fp = sp;
    let mut suppress = suppress;
    let mut old_address = 0;

    loop {
        unsafe {
            let address = sanitize_address((fp as *const u32).offset(-4).read_volatile()); // RA/PC
            fp = (fp as *const u32).offset(-3).read_volatile(); // next FP

            if old_address == address {
                break;
            }

            old_address = address;

            // the address is 0 but we sanitized the address - then 0 becomes 0x40000000
            if address == 0x40000000 {
                break;
            }

            if !crate::is_valid_ram_address(fp) {
                break;
            }

            if fp == 0 {
                break;
            }

            if suppress == 0 {
                result[index] = Some(address as usize);
                index += 1;

                if index >= MAX_BACKTRACE_ADDRESSES {
                    break;
                }
            } else {
                suppress -= 1;
            }
        }
    }

    result
}
