//! FMI 2.0 Co-Simulation C ABI.
//!
//! Exports the standard `fmi2*` symbols expected by an FMI 2.0 Co-Sim host
//! (TwinCAT, B&R, Siemens TIA, Codesys, fmpy, OpenModelica, ...). Each
//! `fmi2Component` is a heap-allocated [`crate::model::FmuInstance`] handed
//! back to the host as an opaque pointer.
//!
//! Only Co-Simulation is supported (the host drives the real-time scheduler;
//! this FMU just re-solves the steady cycle on each `fmi2DoStep`).

use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_uint, c_void};
use std::sync::Mutex;

use libc::{c_double, size_t};

use crate::embedded;
use crate::model::{FmiStatus, FmuInstance};

pub type fmi2Real = c_double;
pub type fmi2Integer = c_int;
pub type fmi2Boolean = c_int;
pub type fmi2String = *const c_char;
pub type fmi2ValueReference = c_uint;
pub type fmi2Time = c_double;
pub type fmi2Component = *mut c_void;
pub type fmi2Status = c_int;

pub const FMI2_OK: fmi2Status = 0;
pub const FMI2_WARNING: fmi2Status = 1;
pub const FMI2_DISCARD: fmi2Status = 2;
pub const FMI2_ERROR: fmi2Status = 3;
pub const FMI2_FATAL: fmi2Status = 4;
pub const FMI2_PENDING: fmi2Status = 5;

pub const FMI2_TRUE: fmi2Boolean = 1;
pub const FMI2_FALSE: fmi2Boolean = 0;

/// FMI 2.0 callback functions. We accept the struct for ABI compatibility but
/// do not invoke the callbacks in this skeleton (the host owns memory/logging).
#[repr(C)]
pub struct fmi2CallbackFunctions {
    pub logger: *mut c_void,
    pub allocate_memory: *mut c_void,
    pub free_memory: *mut c_void,
    pub step_finished: *mut c_void,
    pub intermediate_update: *mut c_void,
}

fn status_to_i32(s: FmiStatus) -> fmi2Status {
    s.as_i32()
}

/// Global registry of last error messages per instance (for host diagnostics).
static LAST_ERRORS: Mutex<Option<HashMap<usize, String>>> = Mutex::new(None);

fn record_error(ptr: usize, msg: String) {
    let mut guard = LAST_ERRORS.lock().unwrap();
    guard.get_or_insert_with(HashMap::new).insert(ptr, msg);
}

/// FMI 2.0 types platform for Co-Simulation.
#[no_mangle]
pub unsafe extern "C" fn fmi2GetTypesPlatform() -> fmi2String {
    static PLATFORM: &[u8] = b"default\0";
    PLATFORM.as_ptr() as *const c_char
}

/// FMI standard version string.
#[no_mangle]
pub unsafe extern "C" fn fmi2GetVersion() -> fmi2String {
    static VERSION: &[u8] = b"2.0\0";
    VERSION.as_ptr() as *const c_char
}

/// Instantiate one FMU. The model JSON + IO-map JSON are loaded via
/// [`crate::embedded::load_model`]: embedded at build time (codegen path) or
/// from `ENTROPYK_FMU_MODEL` / `ENTROPYK_FMU_IO` env vars (dev path).
#[no_mangle]
pub unsafe extern "C" fn fmi2Instantiate(
    _instance_name: fmi2String,
    fmu_type: fmi2String,
    _callback_functions: *const fmi2CallbackFunctions,
    _visible: fmi2Boolean,
    _logging: fmi2Boolean,
) -> fmi2Component {
    // Skeleton supports Co-Simulation only.
    if !fmu_type.is_null() {
        if let Ok(t) = CStr::from_ptr(fmu_type).to_str() {
            if t != "CoSimulation" {
                return std::ptr::null_mut();
            }
        }
    }

    match embedded::load_model() {
        Ok((model_json, io_json)) => match FmuInstance::new(&model_json, &io_json) {
            Ok(instance) => Box::into_raw(Box::new(instance)) as fmi2Component,
            Err(e) => {
                record_error(0, e);
                std::ptr::null_mut()
            }
        },
        Err(e) => {
            record_error(0, e);
            std::ptr::null_mut()
        }
    }
}

/// Release an instance.
#[no_mangle]
pub unsafe extern "C" fn fmi2FreeInstance(c: fmi2Component) {
    if !c.is_null() {
        unsafe { drop(Box::from_raw(c as *mut FmuInstance)) };
    }
}

/// Helper to access the instance behind a component handle.
unsafe fn instance(c: fmi2Component) -> Option<&'static mut FmuInstance> {
    if c.is_null() {
        return None;
    }
    unsafe { Some(&mut *(c as *mut FmuInstance)) }
}

#[no_mangle]
pub unsafe extern "C" fn fmi2SetDebugLogging(
    _c: fmi2Component,
    _logging: fmi2Boolean,
    _n: size_t,
    _categories: *const c_uint,
) -> fmi2Status {
    FMI2_OK
}

#[no_mangle]
pub unsafe extern "C" fn fmi2SetupExperiment(
    _c: fmi2Component,
    _tolerance_defined: fmi2Boolean,
    _tolerance: fmi2Real,
    _start_time: fmi2Real,
    _stop_time_defined: fmi2Boolean,
    _stop_time: fmi2Real,
) -> fmi2Status {
    FMI2_OK
}

#[no_mangle]
pub unsafe extern "C" fn fmi2EnterInitializationMode(c: fmi2Component) -> fmi2Status {
    match unsafe { instance(c) } {
        Some(inst) => status_to_i32(inst.enter_init()),
        None => FMI2_ERROR,
    }
}

#[no_mangle]
pub unsafe extern "C" fn fmi2ExitInitializationMode(c: fmi2Component) -> fmi2Status {
    // Outputs are already populated by enter_init; nothing more to do.
    if c.is_null() {
        FMI2_ERROR
    } else {
        FMI2_OK
    }
}

#[no_mangle]
pub unsafe extern "C" fn fmi2Terminate(c: fmi2Component) -> fmi2Status {
    if c.is_null() {
        FMI2_ERROR
    } else {
        FMI2_OK
    }
}

#[no_mangle]
pub unsafe extern "C" fn fmi2Reset(c: fmi2Component) -> fmi2Status {
    // Re-run a cold solve from current inputs.
    match unsafe { instance(c) } {
        Some(inst) => status_to_i32(inst.do_step()),
        None => FMI2_ERROR,
    }
}

#[no_mangle]
pub unsafe extern "C" fn fmi2GetReal(
    c: fmi2Component,
    vr: *const fmi2ValueReference,
    n: size_t,
    value: *mut fmi2Real,
) -> fmi2Status {
    let inst = match unsafe { instance(c) } {
        Some(i) => i,
        None => return FMI2_ERROR,
    };
    if vr.is_null() || value.is_null() {
        return FMI2_ERROR;
    }
    let vrs = unsafe { std::slice::from_raw_parts(vr, n) };
    let out = unsafe { std::slice::from_raw_parts_mut(value, n) };
    let mut all_ok = true;
    for (k, &v) in vrs.iter().enumerate() {
        match inst.get_real(v) {
            Ok(x) => out[k] = x,
            Err(_) => {
                out[k] = f64::NAN;
                all_ok = false;
            }
        }
    }
    if all_ok {
        FMI2_OK
    } else {
        FMI2_WARNING
    }
}

#[no_mangle]
pub unsafe extern "C" fn fmi2SetReal(
    c: fmi2Component,
    vr: *const fmi2ValueReference,
    n: size_t,
    value: *const fmi2Real,
) -> fmi2Status {
    let inst = match unsafe { instance(c) } {
        Some(i) => i,
        None => return FMI2_ERROR,
    };
    if vr.is_null() || value.is_null() {
        return FMI2_ERROR;
    }
    let vrs = unsafe { std::slice::from_raw_parts(vr, n) };
    let vals = unsafe { std::slice::from_raw_parts(value, n) };
    let mut all_ok = true;
    for (k, &v) in vrs.iter().enumerate() {
        if status_to_i32(inst.set_real(v, vals[k])) != FMI2_OK {
            all_ok = false;
        }
    }
    if all_ok {
        FMI2_OK
    } else {
        FMI2_ERROR
    }
}

#[no_mangle]
pub unsafe extern "C" fn fmi2GetInteger(
    _c: fmi2Component,
    _vr: *const fmi2ValueReference,
    _n: size_t,
    _value: *mut fmi2Integer,
) -> fmi2Status {
    FMI2_ERROR
}

#[no_mangle]
pub unsafe extern "C" fn fmi2SetInteger(
    _c: fmi2Component,
    _vr: *const fmi2ValueReference,
    _n: size_t,
    _value: *const fmi2Integer,
) -> fmi2Status {
    FMI2_ERROR
}

#[no_mangle]
pub unsafe extern "C" fn fmi2GetBoolean(
    _c: fmi2Component,
    _vr: *const fmi2ValueReference,
    _n: size_t,
    _value: *mut fmi2Boolean,
) -> fmi2Status {
    FMI2_ERROR
}

#[no_mangle]
pub unsafe extern "C" fn fmi2SetBoolean(
    _c: fmi2Component,
    _vr: *const fmi2ValueReference,
    _n: size_t,
    _value: *const fmi2Boolean,
) -> fmi2Status {
    FMI2_ERROR
}

#[no_mangle]
pub unsafe extern "C" fn fmi2GetString(
    _c: fmi2Component,
    _vr: *const fmi2ValueReference,
    _n: size_t,
    _value: *mut fmi2String,
) -> fmi2Status {
    FMI2_ERROR
}

#[no_mangle]
pub unsafe extern "C" fn fmi2SetString(
    _c: fmi2Component,
    _vr: *const fmi2ValueReference,
    _n: size_t,
    _value: *const fmi2String,
) -> fmi2Status {
    FMI2_ERROR
}

/// Advance the model by one communication step. Entropyk is steady-state, so
/// `currentCommunicationPoint` and `communicationStepSize` are accepted but
/// ignored: each step re-solves the cycle at the current inputs (quasi-steady
/// co-simulation).
#[no_mangle]
pub unsafe extern "C" fn fmi2DoStep(
    c: fmi2Component,
    _current_communication_point: fmi2Real,
    _communication_step_size: fmi2Real,
    _no_set_fmu_state_prior_to_current_point: fmi2Boolean,
) -> fmi2Status {
    match unsafe { instance(c) } {
        Some(inst) => status_to_i32(inst.do_step()),
        None => FMI2_ERROR,
    }
}

#[no_mangle]
pub unsafe extern "C" fn fmi2CancelStep(_c: fmi2Component) -> fmi2Status {
    FMI2_ERROR
}

/// Non-standard helper for the host to retrieve the last error message as a C
/// string (null-terminated). Returns null if no error. Caller must NOT free.
#[no_mangle]
pub unsafe extern "C" fn fmi2GetLastError() -> fmi2String {
    static mut BUF: Option<CString> = None;
    let msg = LAST_ERRORS
        .lock()
        .unwrap()
        .as_ref()
        .and_then(|m| m.values().last().cloned());
    match msg {
        Some(s) => {
            // Rebuild the static buffer each call (single-threaded host query).
            let c = CString::new(s).unwrap_or_default();
            let ptr = c.as_ptr();
            unsafe { BUF = Some(c) };
            // Keep the CString alive for the caller's read by leaking the old
            // value only after the pointer is handed out. The static retains it.
            ptr
        }
        None => std::ptr::null(),
    }
}
