/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

//! Types needed to marshal [`server`](crate::server) errors back to C++ in Firefox. The main type
//! of this module is [`ErrorBuffer`](crate::server::ErrorBuffer).

use std::{
    borrow::Cow,
    error::Error,
    fmt::{self, Display, Formatter},
    os::raw::c_char,
    ptr,
};

use nsstring::nsCString;
use serde::{Deserialize, Serialize};
use wgc::id;
use wgt::error::{ErrorType, WebGpuError};

/// A non-owning representation of `mozilla::webgpu::ErrorBuffer` in C++, passed as an argument to
/// other functions in [this module](self).
///
/// C++ callers of Rust functions (presumably in `WebGPUParent.cpp`) that expect one of these
/// structs can create a `mozilla::webgpu::ErrorBuffer` object, and call its `ToFFI` method to
/// construct a value of this type, available to C++ as `mozilla::webgpu::ffi::WGPUErrorBuffer`. If
/// we catch a `Result::Err` in other functions of [this module](self), the error is converted to
/// this type.
#[repr(C)]
pub struct ErrorBuffer {
    /// The type of error that `string` is associated with. If this location is set to
    /// [`ErrorBufferType::None`] after being passed as an argument to a function in [this module](self),
    /// then the remaining fields are guaranteed to not have been altered by that function from
    /// their original state.
    r#type: *mut ErrorBufferType,
    /// The (potentially truncated) message associated with this error. A fixed-capacity,
    /// null-terminated UTF-8 string buffer owned by C++.
    ///
    /// When we convert WGPU errors to this type, we render the error as a string, copying into
    /// `message` up to `capacity - 1`, and null-terminate it.
    message: *mut c_char,
    message_capacity: usize,
    device_id: *mut wgc::id::DeviceId,
}

impl ErrorBuffer {
    /// Fill this buffer with the textual representation of `error`.
    ///
    /// If the error message is too long, truncate it to `self.capacity`. In either case, the error
    /// message is always terminated by a zero byte.
    ///
    /// Note that there is no explicit indication of the message's length, only the terminating zero
    /// byte. If the textual form of `error` itself includes a zero byte (as Rust strings can), then
    /// the C++ code receiving this error message has no way to distinguish that from the
    /// terminating zero byte, and will see the message as shorter than it is.
    pub(crate) fn init(&mut self, error: impl WebGpuError, device_id: wgc::id::DeviceId) {
        let err_ty = match error.webgpu_error_type() {
            ErrorType::Internal => ErrorBufferType::Internal,
            ErrorType::OutOfMemory => ErrorBufferType::OutOfMemory,
            ErrorType::Validation => ErrorBufferType::Validation,
            ErrorType::DeviceLost => return, // will be surfaced via callback
        };
        // SAFETY: We presume the pointer provided by the caller is safe to write to.
        unsafe { *self.r#type = err_ty };

        unsafe { *self.device_id = device_id };

        let message = error_to_string(&error);

        assert_ne!(self.message_capacity, 0);
        // Since we need to store a nul terminator after the content, the
        // content length must always be strictly less than the buffer's
        // capacity.
        let length = if message.len() >= self.message_capacity {
            // Thanks to the structure of UTF-8, `std::is_char_boundary` is
            // O(1), so this should examine a few bytes at most.
            //
            // The largest value in this range is `self.message_capacity - 1`,
            // which is a safe length.
            let truncated_length = (0..self.message_capacity)
                .rfind(|&offset| message.is_char_boundary(offset))
                .unwrap_or(0);
            log::warn!(
                "Error message's length {} reached capacity {}, truncating to {}",
                message.len(),
                self.message_capacity,
                truncated_length,
            );
            truncated_length
        } else {
            message.len()
        };
        unsafe {
            ptr::copy_nonoverlapping(message.as_ptr(), self.message as *mut u8, length);
            *self.message.add(length) = 0;
        }
    }
}

pub struct OwnedErrorBuffer {
    device_id: Option<id::DeviceId>,
    ty: ErrorBufferType,
    message: nsCString,
}

impl OwnedErrorBuffer {
    pub fn new() -> Self {
        Self {
            device_id: None,
            ty: ErrorBufferType::None,
            message: nsCString::new(),
        }
    }

    pub(crate) fn init(&mut self, error: impl WebGpuError, device_id: id::DeviceId) {
        assert!(self.device_id.is_none());

        let ty = match error.webgpu_error_type() {
            ErrorType::Internal => ErrorBufferType::Internal,
            ErrorType::OutOfMemory => ErrorBufferType::OutOfMemory,
            ErrorType::Validation => ErrorBufferType::Validation,
            ErrorType::DeviceLost => return, // will be surfaced via callback
        };

        self.device_id = Some(device_id);
        self.ty = ty;

        let message = error_to_string(&error);
        self.message = nsCString::from(message);
    }

    pub(crate) fn get_inner_data(&self) -> Option<(id::DeviceId, ErrorBufferType, &nsCString)> {
        Some((self.device_id?, self.ty, &self.message))
    }
}

pub fn error_to_string(error: impl Error) -> String {
    use std::fmt::Write;
    let mut message = format!("{}", error);
    let mut e = error.source();
    while let Some(source) = e {
        write!(message, ", caused by: {}", source).unwrap();
        e = source.source();
    }
    message
}

/// Corresponds to an optional discriminant of [`GPUError`] type in the WebGPU API. Strongly
/// correlates to [`GPUErrorFilter`]s.
///
/// [`GPUError`]: https://gpuweb.github.io/gpuweb/#gpuerror
/// [`GPUErrorFilter`]: https://gpuweb.github.io/gpuweb/#enumdef-gpuerrorfilter
#[repr(u8)]
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub(crate) enum ErrorBufferType {
    None = 0,
    DeviceLost = 1,
    Internal = 2,
    OutOfMemory = 3,
    Validation = 4,
}

/// Representation an error whose error message is already rendered as a [`&str`], and has no error
/// sources. Used for convenience in [`server`](crate::server) code.
#[derive(Clone, Debug)]
pub(crate) struct ErrMsg {
    pub(crate) message: Cow<'static, str>,
    pub(crate) r#type: ErrorType,
}

impl ErrMsg {
    pub fn oom() -> Self {
        Self {
            message: "Out of memory".into(),
            r#type: ErrorType::OutOfMemory,
        }
    }
}

impl Display for ErrMsg {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let Self { message, r#type: _ } = self;
        write!(f, "{message}")
    }
}

impl Error for ErrMsg {}

impl WebGpuError for ErrMsg {
    fn webgpu_error_type(&self) -> ErrorType {
        self.r#type
    }
}
