# 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.

import ctypes
import os
from enum import Enum


class Status(Enum):
  '''Enumeration for status codes (oms_status_enu_t).'''
  ok = 0
  warning = 1
  discard = 2
  error = 3
  fatal = 4
  pending = 5

class capi:
  def __init__(self):
    self._suppressPathSet = False
    dirname = os.path.dirname(__file__)
    ## look for dll in the current directory for the python pip package
    omslib = os.path.join(dirname, "OMSimulator.dll")

    ## look for dll in the OpenModelica top level directory or OMSimulator stand alone directory
    if not os.path.exists(omslib):
      if os.name == 'nt': # Windows
        omslib = os.path.join(dirname, "../../bin", "OMSimulator.dll")
        self._dllDir = os.add_dll_directory(os.path.dirname(omslib))
      else:
        # attempt to fix #8163 on Linux
        omslib = os.path.join(dirname, "..", "OMSimulator.dll")

    self.obj=ctypes.CDLL(omslib)


    self.obj.oms_addConnection.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool]
    self.obj.oms_addConnection.restype = ctypes.c_int
    self.obj.oms_addConnector.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_int]
    self.obj.oms_addConnector.restype = ctypes.c_int
    self.obj.oms_addSubModel.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    self.obj.oms_addSubModel.restype = ctypes.c_int
    self.obj.oms_addSystem.argtypes = [ctypes.c_char_p, ctypes.c_int]
    self.obj.oms_addSystem.restype = ctypes.c_int
    self.obj.oms_addSignalsToResults.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    self.obj.oms_addSignalsToResults.restype = ctypes.c_int
    self.obj.oms_compareSimulationResults.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_double, ctypes.c_double]
    self.obj.oms_compareSimulationResults.restype = ctypes.c_int
    self.obj.oms_delete.argtypes = [ctypes.c_char_p]
    self.obj.oms_delete.restype = ctypes.c_int
    self.obj.oms_doStep.argtypes = [ctypes.c_char_p]
    self.obj.oms_doStep.restype = ctypes.c_int
    self.obj.oms_getVersion.argtypes = None
    self.obj.oms_getVersion.restype = ctypes.c_char_p
    self.obj.oms_getBoolean.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_bool)]
    self.obj.oms_getBoolean.restype = ctypes.c_int
    self.obj.oms_getInteger.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_int)]
    self.obj.oms_getInteger.restype = ctypes.c_int
    self.obj.oms_getReal.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)]
    self.obj.oms_getReal.restype = ctypes.c_int
    self.obj.oms_getTime.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)]
    self.obj.oms_getTime.restype = ctypes.c_int
    self.obj.oms_getStartTime.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)]
    self.obj.oms_getStartTime.restype = ctypes.c_int
    self.obj.oms_getStopTime.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)]
    self.obj.oms_getStopTime.restype = ctypes.c_int
    self.obj.oms_getString.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p)]
    self.obj.oms_getString.restype = ctypes.c_int
    self.obj.oms_getVariableType.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_int)]
    self.obj.oms_getVariableType.restype = ctypes.c_int
    self.obj.oms_getResultFileSignals.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p)]
    self.obj.oms_getResultFileSignals.restype = ctypes.c_int
    self.obj.oms_initialize.argtypes = [ctypes.c_char_p]
    self.obj.oms_initialize.restype = ctypes.c_int
    self.obj.oms_instantiate.argtypes = [ctypes.c_char_p]
    self.obj.oms_instantiate.restype = ctypes.c_int
    self.obj.oms_newModel.argtypes = [ctypes.c_char_p]
    self.obj.oms_newModel.restype = ctypes.c_int
    self.obj.oms_removeSignalsFromResults.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    self.obj.oms_removeSignalsFromResults.restype = ctypes.c_int
    self.obj.oms_reset.argtypes = [ctypes.c_char_p]
    self.obj.oms_reset.restype = ctypes.c_int
    self.obj.oms_setCommandLineOption.argtypes = [ctypes.c_char_p]
    self.obj.oms_setCommandLineOption.restype = ctypes.c_int
    self.obj.oms_setConnectionLinearTransformation.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_double, ctypes.c_double]
    self.obj.oms_setConnectionLinearTransformation.restype = ctypes.c_int
    self.obj.oms_setConnectorNumericType.argtypes = [ctypes.c_char_p, ctypes.c_int]
    self.obj.oms_setConnectorNumericType.restype = ctypes.c_int
    self.obj.oms_setTempDirectory.argtypes = [ctypes.c_char_p]
    self.obj.oms_setTempDirectory.restype = ctypes.c_int
    self.obj.oms_setExportName.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    self.obj.oms_setExportName.restype = ctypes.c_int
    self.obj.oms_setAliasName.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    self.obj.oms_setAliasName.restype = ctypes.c_int
    self.obj.oms_setBoolean.argtypes = [ctypes.c_char_p, ctypes.c_bool]
    self.obj.oms_setBoolean.restype = ctypes.c_int
    self.obj.oms_setInteger.argtypes = [ctypes.c_char_p, ctypes.c_int]
    self.obj.oms_setInteger.restype = ctypes.c_int
    self.obj.oms_setReal.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_setReal.restype = ctypes.c_int
    self.obj.oms_setString.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    self.obj.oms_setString.restype = ctypes.c_int
    self.obj.oms_setSolver.argtypes = [ctypes.c_char_p, ctypes.c_int]
    self.obj.oms_setSolver.restype = ctypes.c_int
    self.obj.oms_setResultFile.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int]
    self.obj.oms_setResultFile.restype = ctypes.c_int
    self.obj.oms_setStartTime.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_setStartTime.restype = ctypes.c_int
    self.obj.oms_setStopTime.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_setStopTime.restype = ctypes.c_int
    self.obj.oms_setTolerance.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_setTolerance.restype = ctypes.c_int
    self.obj.oms_setFixedStepSize.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_setFixedStepSize.restype = ctypes.c_int
    self.obj.oms_setVariableStepSize.argtypes = [ctypes.c_char_p, ctypes.c_double, ctypes.c_double, ctypes.c_double]
    self.obj.oms_setVariableStepSize.restype = ctypes.c_int
    self.obj.oms_setLogFile.argtypes = [ctypes.c_char_p]
    self.obj.oms_setLogFile.restype = ctypes.c_int
    self.obj.oms_setWorkingDirectory.argtypes = [ctypes.c_char_p]
    self.obj.oms_setWorkingDirectory.restype = ctypes.c_int
    self.obj.oms_setLoggingInterval.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_setLoggingInterval.restype = ctypes.c_int
    self.obj.oms_setLoggingLevel.argtypes = [ctypes.c_int]
    self.obj.oms_setLoggingLevel.restype = ctypes.c_int
    self.obj.oms_simulate.argtypes = [ctypes.c_char_p]
    self.obj.oms_simulate.restype = ctypes.c_int
    self.obj.oms_stepUntil.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_stepUntil.restype = ctypes.c_int
    self.obj.oms_terminate.argtypes = [ctypes.c_char_p]
    self.obj.oms_terminate.restype = ctypes.c_int

  def addConnection(self, crefA, crefB, suppressUnit=False):
    status = self.obj.oms_addConnection(crefA.encode(), crefB.encode(), suppressUnit)
    return Status(status)

  def addConnector(self, cref, causality : int, signal_type : int):
    status = self.obj.oms_addConnector(cref.encode(), causality, signal_type)
    return Status(status)

  def addSubModel(self, model_name: str, submodel_path: str) -> Status:
    '''Add a submodel to the current model.'''
    status = self.obj.oms_addSubModel(model_name.encode('utf-8'), submodel_path.encode('utf-8'))
    return Status(status)

  def addSystem(self, system_name: str, system_type: int) -> Status:
    '''Add a system to the current model.
    system_type: 0 for system, 1 for sub-system.'''
    status = self.obj.oms_addSystem(system_name.encode('utf-8'), system_type)
    return Status(status)

  def addSignalsToResults(self, cref, regex):
    status = self.obj.oms_addSignalsToResults(cref.encode(), regex.encode())
    return Status(status)

  def compareSimulationResults(self, filenameA, filenameB, varA, varB, relTol, absTol):
    result = self.obj.oms_compareSimulationResults(filenameA.encode(), filenameB.encode(), varA.encode(), varB.encode(), relTol, absTol)
    return result

  def delete(self, cref):
    status = self.obj.oms_delete(cref.encode())
    return Status(status)

  def doStep(self, cref):
    status = self.obj.oms_doStep(cref.encode())
    return Status(status)

  def getVersion(self):
    return self.obj.oms_getVersion().decode('utf-8')

  def getReal(self, cref):
    value = ctypes.c_double()
    status = self.obj.oms_getReal(cref.encode(), ctypes.byref(value))
    return [value.value, Status(status)]

  def getInteger(self, cref):
    value = ctypes.c_int()
    status = self.obj.oms_getInteger(cref.encode(), ctypes.byref(value))
    return [value.value, Status(status)]

  def getString(self, cref):
    value = ctypes.c_char_p()
    status = self.obj.oms_getString(cref.encode(), ctypes.byref(value))
    value_ = value.value.decode('utf-8') if value.value else None
    self.obj.oms_freeMemory(value)
    return [value_, Status(status)]

  def getBoolean(self, cref):
    value = ctypes.c_bool()
    status = self.obj.oms_getBoolean(cref.encode(), ctypes.byref(value))
    return [value.value, Status(status)]

  def getResultFileSignals(self, cref):
    value = ctypes.c_char_p()
    status = self.obj.oms_getResultFileSignals(cref.encode(), ctypes.byref(value))
    signals = value.value.decode('utf-8') if value.value else None
    self.obj.oms_freeMemory(value)
    return [signals, Status(status)]

  def getVariableType(self, cref):
    value = ctypes.c_int()
    status = self.obj.oms_getVariableType(cref.encode(), ctypes.byref(value))
    return [value.value, Status(status)]

  def initialize(self, cref) -> Status:
    '''Enters initialization mode.'''
    status = self.obj.oms_initialize(cref.encode())
    return Status(status)

  def instantiate(self, cref):
    status = self.obj.oms_instantiate(cref.encode())
    return Status(status)

  def newModel(self, model_name: str = "default") -> Status:
    '''Create a new model with the given name.'''
    status = self.obj.oms_newModel(model_name.encode('utf-8'))
    return Status(status)

  def setCommandLineOption(self, cmd):
    status = self.obj.oms_setCommandLineOption(cmd.encode())
    return Status(status)

  def setSuppressPath(self, value: bool = True) -> None:
    '''Set --suppressPath once per process; the first caller decides.

    The native flag parser warns about a flag that is set twice.
    '''
    if self._suppressPathSet:
      return
    status = self.setCommandLineOption(f'--suppressPath={str(value).lower()}')
    if status != Status.ok:
      raise RuntimeError(f'Failed to set --suppressPath: {status}')
    self._suppressPathSet = True

  def setConnectionLinearTransformation(self, crefA, crefB, factor, offset):
    '''Set the linear transformation for a connection between two connectors.
    The linear transformation is defined as: output = factor * input + offset.'''
    status = self.obj.oms_setConnectionLinearTransformation(crefA.encode(), crefB.encode(), factor, offset)
    return Status(status)

  def setConnectorNumericType(self, cref, numericType):
    '''Set the numeric type for a connector.
    This is used to specify the numeric type of the connector, which is important for FMI3 connectors that can have different numeric types (e.g., float32, float64, int32, int64).'''
    status = self.obj.oms_setConnectorNumericType(cref.encode(), numericType)
    return Status(status)

  def setTempDirectory(self, newTempDir):
    status = self.obj.oms_setTempDirectory(newTempDir.encode())
    return Status(status)

  def setExportName(self, cref, exportName):
    '''Set the export name for a model or system.
    This is used to specify the name under which the model will be exported.'''
    status = self.obj.oms_setExportName(cref.encode(), exportName.encode())
    return Status(status)

  def setAliasName(self, cref, aliasName):
    '''Set the export name for top level system and sub-system connectors
    This is used to specify the name under which the model will be exported.'''
    status = self.obj.oms_setAliasName(cref.encode(), aliasName.encode())
    return Status(status)

  def setReal(self, cref, value):
    '''Set a real value for a model or system.'''
    status = self.obj.oms_setReal(cref.encode(), value)
    return Status(status)

  def setInteger(self, cref, value):
    '''Set a integer value for a model or system.'''
    status = self.obj.oms_setInteger(cref.encode(), value)
    return Status(status)

  def setBoolean(self, cref, value):
    '''Set a boolean value for a model or system.'''
    status = self.obj.oms_setBoolean(cref.encode(), value)
    return Status(status)

  def setString(self, cref, value):
    '''Set a string value for a model or system.'''
    status = self.obj.oms_setString(cref.encode(), value.encode())
    return Status(status)

  def setSolver(self, cref, solver):
    status = self.obj.oms_setSolver(cref.encode(), solver)
    return Status(status)

  def setResultFile(self, cref, filename, bufferSize=10) -> Status:
    '''Set the result file for a model or system.
    The bufferSize specifies how many values are buffered before writing to the file.'''
    status = self.obj.oms_setResultFile(cref.encode(), filename.encode(), bufferSize)
    return Status(status)

  def setStartTime(self, cref, startTime) -> Status:
    '''Set the start time for the simulation.'''
    status = self.obj.oms_setStartTime(cref.encode(), startTime)
    return Status(status)

  def setStopTime(self, cref, stopTime) -> Status:
    '''Set the stop time for the simulation.'''
    status = self.obj.oms_setStopTime(cref.encode(), stopTime)
    return Status(status)

  def getStartTime(self, cref):
    startTime = ctypes.c_double()
    status = self.obj.oms_getStartTime(cref.encode(), ctypes.byref(startTime))
    return [startTime.value, Status(status)]

  def getStopTime(self, cref):
    stopTime = ctypes.c_double()
    status = self.obj.oms_getStopTime(cref.encode(), ctypes.byref(stopTime))
    return [stopTime.value, Status(status)]

  def getTime(self, cref):
    time = ctypes.c_double()
    status = self.obj.oms_getTime(cref.encode(), ctypes.byref(time))
    return [time.value, Status(status)]

  def setTolerance(self, cref, relativeTolerance) -> Status:
    '''Set the relative and absolute tolerance for the simulation.'''
    status = self.obj.oms_setTolerance(cref.encode(), relativeTolerance)
    return Status(status)

  def setFixedStepSize(self, cref, stepSize) -> Status:
    '''Set the variable step size parameters for the simulation.'''
    status = self.obj.oms_setFixedStepSize(cref.encode(), stepSize)
    return Status(status)

  def setVariableStepSize(self, cref, initialStepSize, minStepSize, maxStepSize) -> Status:
    '''Set the variable step size parameters for the simulation.'''
    status = self.obj.oms_setVariableStepSize(cref.encode(), initialStepSize, minStepSize, maxStepSize)
    return Status(status)

  def simulate(self, cref) -> Status:
    '''Exits initialization mode and runs the simulation until stopTime is reached.'''
    status = self.obj.oms_simulate(cref.encode())
    return Status(status)

  def stepUntil(self, cref, stopTime) -> Status:
    '''Step the simulation until the specified stop time.
    Note: If the model is in initialization mode, this will exit initialization mode.'''
    status = self.obj.oms_stepUntil(cref.encode(), stopTime)
    return Status(status)

  def setLogFile(self, filename):
    status = self.obj.oms_setLogFile(filename.encode())
    return Status(status)

  def setWorkingDirectory(self, newWorkingDir):
    status = self.obj.oms_setWorkingDirectory(newWorkingDir.encode())
    return Status(status)

  def setLoggingInterval(self, cref, loggingInterval):
    status = self.obj.oms_setLoggingInterval(cref.encode(), loggingInterval)
    return Status(status)

  def setLoggingLevel(self, level):
    status = self.obj.oms_setLoggingLevel(level)
    return Status(status)

  def terminate(self, cref) -> Status:
    '''Terminate the simulation and free resources.'''
    status = self.obj.oms_terminate(cref.encode())
    return Status(status)

  def removeSignalsFromResults(self, cref, regex):
    status = self.obj.oms_removeSignalsFromResults(cref.encode(), regex.encode())
    return Status(status)

  def reset(self, cref) -> Status:
    '''Reset the model to the state right after instantiation, discarding initialization/simulation progress.'''
    status = self.obj.oms_reset(cref.encode())
    return Status(status)

Capi = capi()
