# This file is part of OpenModelica.
#
# Copyright (c) 1998-2026, Open Source Modelica Consortium (OSMC),
# c/o Linköpings universitet, Department of Computer and Information Science,
# SE-58183 Linköping, Sweden.
#
# All rights reserved.
#
# THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR
# THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8.
# ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES
# RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL
# VERSION 3, ACCORDING TO RECIPIENTS CHOICE.
#
# The OpenModelica software and the OSMC (Open Source Modelica Consortium)
# Public License (OSMC-PL) are obtained from OSMC, either from the above
# address, from the URLs:
# http://www.openmodelica.org or
# https://github.com/OpenModelica/ or
# http://www.ida.liu.se/projects/OpenModelica,
# and in the OpenModelica distribution.
#
# GNU AGPL version 3 is obtained from:
# https://www.gnu.org/licenses/licenses.html#GPL
#
# This program is distributed WITHOUT ANY WARRANTY; without
# even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH
# IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL.
#
# See the full OSMC Public License conditions for more details.

from OMSimulator.capi import Capi, Status
from OMSimulator.component import Component
from OMSimulator.componenttable import ComponentTable
from OMSimulator.cref import CRef
from OMSimulator.system import System
from OMSimulator.values import Values
from OMSimulator.ssm import SSM
from OMSimulator.variable import Causality, SignalType, SignalNumericType
import json
import tempfile
from enum import Enum

class SolverType(Enum):
  '''Enumeration for solver method to map with c api.'''
  euler = 2
  cvode = 3
  oms_ma = 6
  oms_mav = 7
  oms_mav2 = 8
class SystemType(Enum):
  '''Enumeration for system type to map with c api.'''
  wc = 1
  sc = 2

# Solver methods that produce a WC system; all others produce SC.
_WC_SOLVER_METHODS = {"oms_ma", "oms_mav", "oms_mav2"}

def _system_type_for_method(method: str) -> SystemType:
  return SystemType.wc if method in _WC_SOLVER_METHODS else SystemType.sc

def _system_type_label(system_type: SystemType) -> str:
  return "oms_system_wc" if system_type == SystemType.wc else "oms_system_sc"

class InstantiatedModel:
  def __init__(self, json_description, system: System, resources: dict):
    #print(f"info: Instantiating model with JSON description:\n{json_description}", flush=True)
    config = json.loads(json_description)
    self.modelName = "model" ## create random name, but we cannot commits test as jenkins will gerate new model name
    self.apiCall = []
    self.mappedCrefs = {}  # Store mapped CRefs associated with their export names
    self.system = system
    self.resources = resources
    self.fmuInstantitated = False

    Capi.setSuppressPath()
    # Set the temporary directory
    status = Capi.setTempDirectory(tempfile.mkdtemp())
    if status != Status.ok:
      raise RuntimeError(f"Failed to set temp directory: {status}")
    # Create a new model
    status = Capi.newModel("model")
    if status != Status.ok:
      raise RuntimeError(f"Failed to create new model: {status}")
    self.apiCall.append(f'oms_newModel("{self.modelName}")')

    # Extract all simulation units
    # processElements guarantees every unit has a solver (CS=oms_ma, ME=cvode by default).
    sim_units = config.get("simulation units", [])
    num_units = len(sim_units)
    # print(f"Number of simulation units: {num_units}", flush=True)

    # --- Determine root system type ---
    # Single unit : root takes that solver's own type (WC or SC directly, no wrapper).
    # Multiple units : always WC root so each solver unit becomes a subsystem.
    if num_units == 1:
      root_type = _system_type_for_method(sim_units[0].get("solver", {}).get("method", ""))
    else:
      root_type = SystemType.wc

    root_label = _system_type_label(root_type)
    status = Capi.addSystem(f"{self.modelName}.root", root_type.value)
    export_name = f"{self.system.name}"  # Use the system's name as the export name for the root system
    if export_name not in self.mappedCrefs:
      self.mappedCrefs[export_name] = f"{self.modelName}.root"
    if status != Status.ok:
      raise RuntimeError(f"Failed to create root system: {status}")
    self.apiCall.append(f'oms_addSystem("{self.modelName}.root", "{root_label}")')

    # Step 1: Create subsystems, add all components, populate mappedCrefs
    # We must add every component before processing any connections, because a
    # connection in one simulation unit may reference a component that lives in a
    # different unit (cross-unit connection at the WC root level).

    unit_solver_paths = []  # parallel list: solver_path for each sim_unit
    for unit in sim_units:
      solver = unit.get("solver", {})
      method = solver.get("method", "")
      # WC-method units (oms_ma etc.) cannot be nested under a WC root —
      # their components go directly into model.root.
      # SC-method units (cvode, euler) get their own named subsystem.
      if num_units > 1 and method not in _WC_SOLVER_METHODS:
        solvername = solver['name']
        solver_path = f"{self.modelName}.root.{solvername}"
        sub_type = _system_type_for_method(method)
        sub_label = _system_type_label(sub_type)
        self.apiCall.append(f'oms_addSystem("{solver_path}", "{sub_label}")')
        status = Capi.addSystem(f"model.root.{solvername}", sub_type.value)
        if status != Status.ok:
          raise RuntimeError(f"Failed to add oms_addSystem: {status}")
      else:
        solver_path = f"{self.modelName}.root"

      ## set solver method and settings if provided
      if solver:
        if method in ("cvode", "euler", "oms_ma", "oms_mav", "oms_mav2"):
          status = Capi.setSolver(solver_path, SolverType[method].value)
          if status != Status.ok:
            raise RuntimeError(f"Failed to set solver: {status}")
          self.apiCall.append(f'oms_setSolver("{solver_path}", "{method}")')

        relativeTolerance = solver.get("relativeTolerance")
        if relativeTolerance is not None:
          status = Capi.setTolerance(solver_path, float(relativeTolerance))
          if status != Status.ok:
            raise RuntimeError(f"Failed to set tolerance: {status}")
          self.apiCall.append(f'oms_setTolerance("{solver_path}", {float(relativeTolerance)})')

        # variable-step solvers: initialStepSize, minimumStepSize, maximumStepSize
        if method in ("cvode", "oms_mav", "oms_mav2"):
          initialStepSize = solver.get("initialStepSize")
          minimumStepSize = solver.get("minimumStepSize")
          maximumStepSize = solver.get("maximumStepSize")
          if any(v is not None for v in (initialStepSize, minimumStepSize, maximumStepSize)):
            initialStepSize = float(initialStepSize) if initialStepSize is not None else 1e-6
            minimumStepSize = float(minimumStepSize) if minimumStepSize is not None else 1e-12
            maximumStepSize = float(maximumStepSize) if maximumStepSize is not None else 0.001
            status = Capi.setVariableStepSize(solver_path, initialStepSize, minimumStepSize, maximumStepSize)
            if status != Status.ok:
              raise RuntimeError(f"Failed to set variable step size: {status}")
            self.apiCall.append(f'oms_setVariableStepSize("{solver_path}", {initialStepSize}, {minimumStepSize}, {maximumStepSize})')

        # fixed-step solvers: fixedStepSize
        if method in ("oms_ma", "euler"):
          fixedStepSize = solver.get("fixedStepSize")
          if fixedStepSize is not None:
            status = Capi.setFixedStepSize(solver_path, float(fixedStepSize))
            if status != Status.ok:
              raise RuntimeError(f"Failed to set fixed step size: {status}")
            self.apiCall.append(f'oms_setFixedStepSize("{solver_path}", {float(fixedStepSize)})')


      ## add components
      for comp in unit["components"]:
        if len(comp["name"]) <= 2:
          comp_path = ".".join([solver_path] + [comp["name"][-1]])
        else:
          ## add prefix to nested systems to avoid name conflicts while flattening the system during instantiation, e.g. sub-system1_Add1, sub-system1_Gain1
          comp_name = f"{comp['name'][-2]}_{comp['name'][-1]}"
          comp_path = ".".join([solver_path] + [comp_name])
        self.apiCall.append(f'oms_addSubModel("{comp_path}", "{comp["path"]}")')
        status = Capi.addSubModel(comp_path, comp["path"])
        if status != Status.ok:
          raise RuntimeError(f"Failed to add oms_addSubModel: {status}")
        export_name = ".".join(comp["name"])

        ## parse connector geometry for the component if exist and set it to capi after adding the component, this is needed for proper mapping of connector geometry
        if "connectors" in comp:
          for connector in comp["connectors"]:
            comp_name = ".".join(comp["name"])
            connector_name =".".join([comp_name]+[connector["name"]])
            if not connector_name in self.mappedCrefs:
              self.mappedCrefs[connector_name] = f"{comp_path}.{connector['name']}"
            if "geometry" in connector:
              connector_path = ".".join([comp_path, connector["name"]])
              geometry = connector["geometry"]
              x = geometry.get("x", 0.0)
              y = geometry.get("y", 0.0)
              status = Capi.setConnectorGeometry(connector_path, x, y)
              if status != Status.ok:
                raise RuntimeError(f"Failed to set connector geometry for {connector_path}: {status}")
              self.apiCall.append(f'oms_setConnectorGeometry("{connector_path}", {x}, {y})')

        if not export_name in self.mappedCrefs:
          self.mappedCrefs[export_name] = comp_path
          self.mappedCrefs[".".join(comp["name"][:-1])] = solver_path # map parent system too for connector lookup
        status = Capi.setExportName(comp_path, export_name)  # Set export name if provided
        if status != Status.ok:
          raise RuntimeError(f"Failed to set export name: {status}")

      unit_solver_paths.append(solver_path)

    # Add connectors for the root system and all subsystems in one recursive pass.
    # This replaces per-unit listofsystems tracking and duplication entirely.
    self._addConnectorsRecursive(self.system, self.system.name)

    # Step 2: Add all connections (mappedCrefs is now fully populated)
    for unit in sim_units:
      for connection in unit["connections"]:
        start_element = ".".join(connection['start element'] + [connection['start connector']])
        end_element = ".".join(connection['end element'] + [connection['end connector']])

        if start_element not in self.mappedCrefs:
          raise KeyError(f"No mapping found for {start_element}")
        if end_element not in self.mappedCrefs:
          raise KeyError(f"No mapping found for {end_element}")

        start = self.mappedCrefs[start_element]
        end = self.mappedCrefs[end_element]
        self.apiCall.append(f'oms_addConnection("{start}", "{end}")')
        status = Capi.addConnection(start, end)
        if status != Status.ok:
          raise RuntimeError(f"Failed to add oms_addConnection: {status}")
        ## add linear transformation if exist
        if "linear transformation" in connection:
          factor = connection["linear transformation"]["factor"]
          offset = connection["linear transformation"]["offset"]
          self.apiCall.append(f'oms_setConnectionLinearTransformation("{start}", "{end}", {factor}, {offset})')
          status = Capi.setConnectionLinearTransformation(start, end, float(factor), float(offset))
          if status != Status.ok:
            raise RuntimeError(f"Failed to set connection linear transformation: {status}")

    ## set start values
    self.setStartValues(self.system.value, self.system.name, self.system.parameterMapping)
    ## set start values from ssv files
    self.setStartValuesFromSSV(self.system.parameterResources, self.system.name)
    ## iterate start values from sub-system both inline and ssv files if exist
    self.setStartValuesFromElements(self.system.elements, self.system.name)

    self.apiCall.append(f'oms_instantiate("{self.modelName}")')
    status = Capi.instantiate(self.modelName)
    if status != Status.ok:
      raise RuntimeError(f"Failed to instantiate model: {status}")
    self.fmuInstantitated = True
    ## set start and stop time from ssp
    self.setStartTime(float(config["simulation settings"]['start time']))
    self.setStopTime(float(config["simulation settings"]['stop time']))
    ## set result file name from ssp
    self.setResultFile(config["simulation settings"]['result file'], int(config["simulation settings"]['buffer size']))
    self.setLoggingInterval(float(config["simulation settings"]['logging interval']))

  def setStartValuesFromElements(self, elements, systemName):
    for key, element in elements.items():
      ## do not set start values for tables
      if isinstance(element, ComponentTable):
        continue
      new_system_name = ".".join([systemName, str(element.name)])
      self.setStartValues(element.value, new_system_name, element.parameterMapping)
      self.setStartValuesFromSSV(element.parameterResources, new_system_name)

      ## recursive for sub-system
      if isinstance(element, System):
        new_system_name = ".".join([systemName, str(element.name)])
        self.setStartValuesFromElements(element.elements, new_system_name)

  def setStartValuesFromSSV(self, parameterResources, systemName):
    if not parameterResources:
      return
    for resource in parameterResources:
      for (key, mapping) in resource.items():
        if key in self.resources:
          ssv = self.resources.get(key)
          ssm = self.resources.get(mapping, None)  # get the ssm mapping if exist
          self.setStartValues(ssv.value, systemName, ssm)
        else:
          raise KeyError(f"Missing required resource: '{key}'")

  def map_cref(self, system_name, var_str):
    ## map cref with the longest path
    ## e.g. default.sub-system.gain1.k -> model.root.gain1.k
    ## e.g. default.sub-system.gain1.R1.T. -> model.root.solver2.gain1.R1.T
    # Combine system name and variable
    cref = f"{system_name}.{var_str}"
    # Split into parts
    parts = cref.split(".")
    ## map with the longest path
    for i in range(len(parts), 0, -1):
      prefix = ".".join(parts[:i])
      if prefix in self.mappedCrefs:
        mapped_prefix = self.mappedCrefs[prefix]
        validate_path = self.validatePath(mapped_prefix.split("."), parts[i:])
        return validate_path

    raise KeyError(f"No mapping found for {cref}")

  def validatePath(self, mapped_prefix : list, suffix : list):
    ## remove common path from suffix
    ## e.g. mapped_prefix = model.root.solver2.gain1
    ##      suffix = gain1.R1.T
    ##      result = model.root.solver2.gain1.R1.T
    result = []
    for item in suffix:
      if item not in mapped_prefix:
        result.append(item)

    return ".".join(mapped_prefix + result)

  def validate_cref(self, systemName: str, suffix: str) -> str:
    ## remove common path from suffix
    ## e.g. systemName = model.root.solver2.gain1
    ##      suffix = gain1.R1.T
    ##      result = model.root.solver2.gain1.R1.T

    a_parts = systemName.split(".")
    b_parts = suffix.split(".")

    # detect overlap
    overlap = 0
    for i in range(1, min(len(a_parts), len(b_parts)) + 1):
      if a_parts[-i:] == b_parts[:i]:
        overlap = i
    cref = ".".join(a_parts + b_parts[overlap:])
    if cref in self.mappedCrefs:
      return self.mappedCrefs[cref]
    else:
      ## it is possible some variables are not listed in connectors but it is still valid signal
      return self.map_cref(systemName, suffix)

  def setStartValues(self, value: Values, systemName: str, ssm: SSM | None):
    if value.empty():
      return

    if systemName not in self.mappedCrefs:
      raise KeyError(f"Missing required key: '{systemName}'")

    ## apply ssm mapping if exist and apply start values from mapped parameters
    if ssm and (ssm.filename or ssm.mappingEntry):
      for source, targets in ssm.mappingEntry.items():
        if CRef(source) in value.start_values:
          (source_value, type, _, _) = value.start_values[CRef(source)]
          for entry in targets:
            target = entry["target"]
            linearTransformation = entry["linearTransformation"]
            cref = f"{systemName}.{str(target)}"
            value_path = self.validate_cref(systemName, str(target))
            if linearTransformation:
              source_value = source_value * float(linearTransformation.factor) + float(linearTransformation.offset)
            self.apply_start_value(value_path, source_value, type)
    else:
      for key, (source_value, type, _, _) in value.start_values.items():
        cref = f"{systemName}.{str(key)}"
        value_path = self.validate_cref(systemName, str(key))
        self.apply_start_value(value_path, source_value, type)

  def apply_start_value(self, value_path:str, value, type):
    # Determine the variable type
    if type is None:
      type, status = Capi.getVariableType(value_path)
      if status != Status.ok:
        raise RuntimeError(f"Failed to get variable type for {value_path}: {status}")
    ## TODO: handle FMi3 data types directly, like Float64, Int32,
    value_ = value.value if hasattr(value, 'value') else value

    match SignalType(type):
      case SignalType.Real | SignalType.Float32 | SignalType.Float64:  # oms_signal_type_real
        self._setReal(value_path, float(value_))
      case SignalType.Integer | SignalType.Int32 | SignalType.Int8 | SignalType.UInt8 | SignalType.Int16 | SignalType.UInt16 | SignalType.Int32 |SignalType.UInt32 | SignalType.Int64 | SignalType.UInt64:  # oms_signal_type_integer
        self._setInteger(value_path, int(value_))
      case SignalType.Boolean:  # oms_signal_type_boolean
        self._setBoolean(value_path, bool(value_))
      case SignalType.String:  # oms_signal_type_string
        self._setString(value_path, str(value_))
      case SignalType.Enumeration:  # oms_signal_type_enumeration
        self._setInteger(value_path, int(value_))  # Treat enumeration as integer
      case _:
        raise TypeError(f"Unsupported type: {type}")

  def _addConnectorsRecursive(self, system: System, system_name: str):
    """Walk the system tree and add connectors for every system/subsystem
    that was actually instantiated (i.e. present in mappedCrefs)."""
    if system_name not in self.mappedCrefs:
      # connector-only subsystems (no FMU components) are not added during Step 1;
      # derive their OMS path from the parent so their connectors can be flattened there.
      parent_name = system_name.rsplit(".", 1)[0] if "." in system_name else None
      if parent_name and parent_name in self.mappedCrefs:
        self.mappedCrefs[system_name] = self.mappedCrefs[parent_name]
      else:
        return
    if system.connectors:
      self._addConnector(system.connectors, system_name)
    for element in system.elements.values():
      if isinstance(element, System):
        child_name = f"{system_name}.{element.name}"
        self._addConnectorsRecursive(element, child_name)

  def _addConnector(self, connectors, systemName):
    for connector in connectors:
      connector_name =".".join([systemName]+[str(connector.name)])
      system_name = systemName.split(".")
      if len(system_name) == 1:
        connector_path = ".".join([self.mappedCrefs[systemName], str(connector.name)])
      else:
        ## add prefix to nested systems to avoid name conflicts while flattening the system during instantiation, e.g. sub-system1_input1, sub-system1_param1
        connector_name_prefix = f"{system_name[-1]}_{str(connector.name)}"  # replace '-' with '_' for sub-system name in connector path
        connector_path = ".".join([self.mappedCrefs[systemName], connector_name_prefix])
      self.apiCall.append(f'oms_addConnector("{connector_path}", "{connector.causality}", {connector.signal_type})')
      status = Capi.addConnector(connector_path, connector.causality.value, connector.c_signal_type.value)
      if status != Status.ok:
        raise RuntimeError(f"Failed to add oms_addConnector:{status}")
      ## set connector numeric type if exist, this is needed for proper handling of FMI3 connectors with different numeric types
      if connector.numericType != SignalNumericType.NONE:
        self.apiCall.append(f'oms_setConnectorNumericType("{connector_path}", {connector.numericType})')
        status = Capi.setConnectorNumericType(connector_path, connector.numericType.value)
        if status != Status.ok:
          raise RuntimeError(f"Failed to set connector numeric type for {connector_path}: {status}")

      export_name = systemName
      if not connector_name in self.mappedCrefs:
        self.mappedCrefs[connector_name] = connector_path
      status = Capi.setExportName(connector_path, export_name)  # Set export name if provided
      if status != Status.ok:
        raise RuntimeError(f"Failed to set export name: {status}")
      status = Capi.setAliasName(connector_path, str(connector.name))  # Set alias name for connector
      if status != Status.ok:
        raise RuntimeError(f"Failed to set alias name: {status}")

  def dumpApiCalls(self):
    """Returns the generated API calls as a string."""
    return "\n".join(self.apiCall)

  def setValue(self, cref: CRef, value):
    """Sets a value for a specific CRef in the model."""
    name = ".".join(cref.names)

    ## check for top level connectors with alias names first
    ## e.g model.root.sub_system_input which will be in mapped crefs
    if name in self.mappedCrefs:
      value_path = self.mappedCrefs[name]
    else:
      ## check for other crefs by splitting with suffixes
      name = ".".join(cref.names[:-1])
      if name not in self.mappedCrefs:
        raise KeyError(f"Missing required key: '{cref.names}'")
      value_path = ".".join([self.mappedCrefs[name], cref.names[-1]])

    # Determine the variable type
    type, status = Capi.getVariableType(value_path)
    if status != Status.ok:
      raise RuntimeError(f"Failed to get variable type for {cref}: {status}")

    value_ = value.value if hasattr(value, 'value') else value
    ## TODO handle FMi3 data types directly, like Float64, Int32,etc..
    match SignalType(type):
      case SignalType.Real:  # oms_signal_type_real
        return self._setReal(value_path, float(value_))
      case SignalType.Integer:  # oms_signal_type_integer
        return self._setInteger(value_path, int(value_))
      case SignalType.Boolean:  # oms_signal_type_boolean
        return self._setBoolean(value_path, bool(value_))
      case SignalType.String:  # oms_signal_type_string
        return self._setString(value_path, str(value_))
      case SignalType.Enumeration:  # oms_signal_type_enumeration
        return self._setInteger(value_path, int(value_))  # Treat enumeration as integer
      case _:
        raise TypeError(f"Unsupported type: {type}")

  def _setReal(self, mapped_cref: str, value: float):
    self.apiCall.append(f'oms_setReal("{mapped_cref}", {value})')
    return Capi.setReal(mapped_cref, value) # Get the value from the CAPI

  def _setInteger(self, mapped_cref: str, value: int):
    self.apiCall.append(f'oms_setInteger("{mapped_cref}", {value})')
    return Capi.setInteger(mapped_cref, value) # Get the value from the CAPI

  def _setBoolean(self, mapped_cref: str, value: bool):
    self.apiCall.append(f'oms_setBoolean("{mapped_cref}", {value})')
    return Capi.setBoolean(mapped_cref, value) # Get the value from the CAPI

  def _setString(self, mapped_cref: str, value: str):
    self.apiCall.append(f'oms_setString("{mapped_cref}", {value})')
    return Capi.setString(mapped_cref, value) # Get the value from the CAPI

  def getValue(self, cref: CRef):
    name = ".".join(cref.names)
    ## check for top level connectors with alias names first
    ## e.g model.root.sub_system_input which will be in mapped crefs
    if name in self.mappedCrefs:
      value_path = self.mappedCrefs[name]
    else:
      ## check for other crefs by splitting with suffixes
      name = ".".join(cref.names[:-1])
      if name not in self.mappedCrefs:
        raise KeyError(f"Missing required key: '{cref.names}'")
      value_path = ".".join([self.mappedCrefs[name], cref.names[-1]])

    # Determine the variable type
    type, status = Capi.getVariableType(value_path)
    if status != Status.ok:
      raise RuntimeError(f"Failed to get variable type for {cref}: {status}")

    match SignalType(type):
      case SignalType.Real:  # oms_signal_type_real
        return self._getReal(value_path)
      case SignalType.Integer:  # oms_signal_type_integer
        return self._getInteger(value_path)
      case SignalType.Boolean:  # oms_signal_type_boolean
        return self._getBoolean(value_path)
      case SignalType.String:  # oms_signal_type_string
        return self._getString(value_path)
      case SignalType.Enumeration:  # oms_signal_type_enumeration
        return self._getInteger(value_path)  # Treat enumeration as integer
      case _:
        raise TypeError(f"Unsupported type: {type}")

  def _getReal(self, cref: CRef):
    (value, _) = Capi.getReal(cref) # Get the value from the CAPI
    return value

  def _getInteger(self, cref: CRef):
    (value, _) = Capi.getInteger(cref)
    return value

  def _getBoolean(self, cref: CRef):
    (value, _) = Capi.getBoolean(cref)
    return value

  def _getString(self, cref: CRef):
    (value, _) = Capi.getString(cref)
    return value

  def initialize(self):
    status = Capi.initialize(self.modelName)
    if status != Status.ok:
      raise RuntimeError(f"Failed to initialize model: {status}")

  def reset(self):
    '''Reset the model to the state right after instantiation, discarding initialization/simulation progress.'''
    status = Capi.reset(self.modelName)
    if status != Status.ok:
      raise RuntimeError(f"Failed to reset model: {status}")

  def simulate(self):
    status = Capi.simulate(self.modelName)
    if status != Status.ok:
      raise RuntimeError(f"Failed to simulate model: {status}")

  def stepUntil(self, stopTime):
    status = Capi.stepUntil(self.modelName, stopTime)
    if status != Status.ok:
      raise RuntimeError(f"Failed to step until {stopTime}: {status}")

  def setResultFile(self, filename: str, bufferSize: int = 10):
    status = Capi.setResultFile("model", filename, bufferSize)
    if status !=Status.ok:
      raise RuntimeError(f"Failed to setResultFile {filename}: {status}")

  def setStartTime(self, startTime: float):
    status = Capi.setStartTime(self.modelName, startTime)
    if status != Status.ok:
      raise RuntimeError(f"Failed to set start time: {status}")

  def getStartTime(self) -> float:
    startTime, status = Capi.getStartTime(self.modelName)
    if status != Status.ok:
      raise RuntimeError(f"Failed to get start time: {status}")
    return startTime

  def getStopTime(self) -> float:
    stopTime, status = Capi.getStopTime(self.modelName)
    if status != Status.ok:
      raise RuntimeError(f"Failed to get stop time: {status}")
    return stopTime

  def getTime(self) -> float:
    time, status = Capi.getTime(self.modelName)
    if status != Status.ok:
      raise RuntimeError(f"Failed to get time: {status}")
    return time


  def doStep(self):
    status = Capi.doStep(self.modelName)
    if status != Status.ok:
      raise RuntimeError(f"Failed to do step: {status}")

  def setStopTime(self, stopTime: float):
    if self.fmuInstantitated is False:
      raise RuntimeError("FMU must be instantiated before setting stop time")

    status = Capi.setStopTime(self.modelName, stopTime)
    if status != Status.ok:
      raise RuntimeError(f"Failed to set stop time: {status}")

  def setTolerance(self, tolerance: float):
    if self.fmuInstantitated is False:
      raise RuntimeError("FMU must be instantiated before setting tolerance")

    status = Capi.setTolerance(f"{self.modelName}.root", tolerance)
    if status != Status.ok:
      raise RuntimeError(f"Failed to set tolerance: {status}")

  def setFixedStepSize(self, stepSize: float):
    if self.fmuInstantitated is False:
      raise RuntimeError("FMU must be instantiated before setting variable step size")

    status = Capi.setFixedStepSize(f"{self.modelName}.root", stepSize)
    if status != Status.ok:
      raise RuntimeError(f"Failed to set fixed step size: {status}")

  def setVariableStepSize(self, initialStepSize: float, minimumStepSize: float, maximumStepSize: float):
    if self.fmuInstantitated is False:
      raise RuntimeError("FMU must be instantiated before setting variable step size")

    status = Capi.setVariableStepSize(f"{self.modelName}.root", initialStepSize, minimumStepSize, maximumStepSize)
    if status != Status.ok:
      raise RuntimeError(f"Failed to set variable step size: {status}")

  def setLoggingLevel(self, level: int):
    status = Capi.setLoggingLevel(level)
    if status != Status.ok:
      raise RuntimeError(f"Failed to set logging level: {status}")

  def setLogFile(self, filename: str):
    status = Capi.setLogFile(filename)
    if status != Status.ok:
      raise RuntimeError(f"Failed to set log file: {status}")

  def setLoggingInterval(self, interval: float):
    status = Capi.setLoggingInterval(self.modelName, interval)
    if status != Status.ok:
      raise RuntimeError(f"Failed to set logging interval: {status}")

  def addSignalsToResults(self, regex: str):
    status = Capi.addSignalsToResults(self.modelName, regex)
    if status != Status.ok:
      raise RuntimeError(f"Failed to set regular expression : {status}")

  def removeSignalsFromResults(self, regex: str):
    status = Capi.removeSignalsFromResults(self.modelName, regex)
    if status != Status.ok:
      raise RuntimeError(f"Failed to set regular expression : {status}")

  def terminate(self):
    status = Capi.terminate(self.modelName)
    if status != Status.ok:
      raise RuntimeError(f"Failed to terminate model: {status}")

  def delete(self):
    status = Capi.delete(self.modelName)
    if status != Status.ok:
      raise RuntimeError(f"Failed to delete model: {status}")
    self.modelName = None  # Clear the model name after deletion
    self.apiCall.clear()  # Clear the API call history
