use crate::conversion::IntoPyObject;
use crate::inspect::PyStaticExpr;

/// Seals `PyReturnType` so that types outside PyO3 cannot implement it.
mod return_type {
    use crate::{impl_::introspection::PyReturnType, IntoPyObject};

    pub trait Sealed {}

    impl<'a, T: IntoPyObject<'a>> Sealed for T {}
    impl<T: PyReturnType, E> Sealed for Result<T, E> {}
}

/// Trait to guess a function Python return type
///
/// It is useful to properly get the return type `T` when the Rust implementation returns e.g. `PyResult<T>`
pub trait PyReturnType: return_type::Sealed {
    /// The function return type
    const OUTPUT_TYPE: PyStaticExpr;
}

impl<'a, T: IntoPyObject<'a>> PyReturnType for T {
    const OUTPUT_TYPE: PyStaticExpr = T::OUTPUT_TYPE;
}

impl<T: PyReturnType, E> PyReturnType for Result<T, E> {
    const OUTPUT_TYPE: PyStaticExpr = T::OUTPUT_TYPE;
}

#[diagnostic::on_unimplemented(
    message = "`{Self}` cannot be converted to a Python object",
    label = "required by `#[pyo3(get)]` to create a readable property from a field of type `{Self}`",
    note = "implement `IntoPyObject` for `&{Self}` or `IntoPyObject + Clone` for `{Self}` to define the conversion"
)]
pub trait PyIntoPyObjectMaybeRefType<const REF_IMPL_EXISTS: bool> {
    const OUTPUT_TYPE: PyStaticExpr;
}

impl<'a, 'py, T: 'a> PyIntoPyObjectMaybeRefType<true> for T
where
    &'a T: IntoPyObject<'py>,
{
    const OUTPUT_TYPE: PyStaticExpr = <&T as IntoPyObject<'_>>::OUTPUT_TYPE;
}

impl<'py, T: IntoPyObject<'py>> PyIntoPyObjectMaybeRefType<false> for T {
    const OUTPUT_TYPE: PyStaticExpr = <T as IntoPyObject<'_>>::OUTPUT_TYPE;
}

#[repr(C)]
pub struct SerializedIntrospectionFragment<const LEN: usize> {
    pub length: u32,
    pub fragment: [u8; LEN],
}

/// Escapes a string to be valid JSON. Does not add quotes around it
///
/// Returns the number of written bytes
pub const fn escape_json_string(input: &str, output: &mut [u8]) -> usize {
    let input = input.as_bytes();
    let mut input_i = 0;
    let mut output_i = 0;
    while input_i < input.len() {
        match input[input_i] {
            b'\\' => {
                output[output_i] = b'\\';
                output_i += 1;
                output[output_i] = b'\\';
                output_i += 1;
            }
            b'"' => {
                output[output_i] = b'\\';
                output_i += 1;
                output[output_i] = b'"';
                output_i += 1;
            }
            0x08 => {
                output[output_i] = b'\\';
                output_i += 1;
                output[output_i] = b'b';
                output_i += 1;
            }
            0x0C => {
                output[output_i] = b'\\';
                output_i += 1;
                output[output_i] = b'f';
                output_i += 1;
            }
            b'\n' => {
                output[output_i] = b'\\';
                output_i += 1;
                output[output_i] = b'n';
                output_i += 1;
            }
            b'\r' => {
                output[output_i] = b'\\';
                output_i += 1;
                output[output_i] = b'r';
                output_i += 1;
            }
            b'\t' => {
                output[output_i] = b'\\';
                output_i += 1;
                output[output_i] = b't';
                output_i += 1;
            }
            c @ 0..32 => {
                output[output_i] = b'\\';
                output_i += 1;
                output[output_i] = b'u';
                output_i += 1;
                output[output_i] = b'0';
                output_i += 1;
                output[output_i] = b'0';
                output_i += 1;
                output[output_i] = b'0' + (c / 16);
                output_i += 1;
                let remainder = c % 16;
                output[output_i] = if remainder >= 10 {
                    b'a' + remainder - 10
                } else {
                    b'0' + remainder
                };
                output_i += 1;
            }
            c => {
                output[output_i] = c;
                output_i += 1;
            }
        }
        input_i += 1;
    }
    output_i
}

/// Number of bytes written by [`escape_json_string`]
pub const fn escaped_json_string_len(input: &str) -> usize {
    let input = input.as_bytes();
    let mut len = 0;
    let mut i = 0;
    while i < input.len() {
        len += match input[i] {
            b'\\' | b'"' | 0x08 | 0x0C | b'\n' | b'\r' | b'\t' => 2,
            0..32 => 6,
            _ => 1,
        };
        i += 1;
    }
    len
}
