# 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 pathlib import Path

from lxml import etree as ET
from OMSimulator.cref import CRef
from OMSimulator.values import Values
from OMSimulator.elementgeometry import ElementGeometry
from OMSimulator import namespace

class Component:
  def __init__(self, name: CRef, fmuPath: Path | str, implementation=None, connectors=None, unitDefinitions=None, enumerationDefinitions=None):
    from OMSimulator.ssm import SSM
    self.name = CRef(name)
    self.fmuPath = Path(fmuPath).as_posix()
    self.connectors = connectors or list()
    self.unitDefinitions = unitDefinitions or list()
    self.enumerationDefinitions = enumerationDefinitions or list()
    self.elementgeometry = None
    self.description = None
    self.value = Values()
    self.parameterMapping = SSM()
    self.solver = None
    self.parameterResources = []
    self.metaDataResources = []
    self.implementation = implementation
    self.fmu = None  # set by addComponent when the FMU is loaded

  def addConnector(self, connector):
    if connector in self.connectors:
      raise ValueError(f"Connector '{connector.name}' already exists in {self.name}")
    self.connectors.append(connector)

  def getConnector(self, cref: CRef):
    for connector in self.connectors:
      if connector.name == cref:
        return connector
    return None

  def deleteConnector(self, cref: CRef):
    for i, connector in enumerate(self.connectors):
      if connector.name == cref:
        del self.connectors[i]
        return
    raise ValueError(f"Connector '{cref}' not found in {self.name}")

  def addSSVReference(self, resource1: str, resource2: str | None = None):
    self.parameterResources.append({resource1: resource2})

  def addMetaDataReference(self, resource: str, kind: str, type: str):
    self.metaDataResources.append({"source":resource, "kind":kind, "type":type})

  def mapParameter(self, source: str, target: str):
    self.parameterMapping.mapParameter(source, target)

  def hasMappingEntry(self, source: str) -> bool:
    return str(source) in self.parameterMapping.mappingEntry

  def swapSSVReference(self, resource1: str, resource2: str):
    self.removeSSVReference(resource1)
    self.addSSVReference(resource2)

  def listSSVReference(self):
    return self.parameterResources

  def exportSSVTemplate(self, node, prefix=None):
    self.value.add_parameters(node, prefix)

  def exportSSMTemplate(self, node, prefix=None):
    self.parameterMapping.exportSSMTemplate(node, self.connectors, prefix)

  def removeSSVReference(self, resource: str, raise_error=True):
    for entry in self.parameterResources:
      for key, value in entry.items():
        if key == resource:
          del entry[key]
          return
    if raise_error:
      raise ValueError(f"Resource '{resource}' not found in {self.name}")

  def list(self, prefix=""):
    print(f"{prefix} FMU: {self.name} '{self.description}'")
    prefix += ' |--'
    print(f"{prefix} path: {self.fmuPath}")

    if len(self.connectors) > 0:
      print(f"{prefix} Connectors:")
      for connector in self.connectors:
        connector.list(prefix=prefix + " |--")

    ## list component element geometry
    if self.elementgeometry:
      print(f"{prefix} ElementGeometry:")
      self.elementgeometry.list(prefix=prefix + " |--")

    ## list parameters inline
    if not self.value.empty():
      print(f"{prefix} Inline Parameter Bindings:")
      self.value.list(prefix=prefix + " |--")
      if not self.parameterMapping.empty():
        print(f"{prefix} |-- Inline Parameter Mapping:")
        self.parameterMapping.list(prefix=prefix + " |-- |--")

      ## list unit definitions
      if len(self.unitDefinitions) > 0:
        print(f"{prefix} |-- UnitDefinitions:")
        for unit in self.unitDefinitions:
          unit.list(prefix=prefix + " |-- |--")

    ## list parameteres in ssv files
    if len(self.parameterResources) > 0:
      for resource in self.parameterResources:
        for key, value in resource.items():
          print(f"{prefix} Parameter Bindings: {key}")
          if value:
            print(f"{prefix} |-- Parameter Mapping: {value}")

    ## list metadata resources
    if len(self.metaDataResources) > 0:
      for resources in self.metaDataResources:
        print(f"{prefix} MetaData: {resources.get('source')}")

    ## list solver settings
    if self.solver:
      print(f"{prefix} Solver Settings:")
      print(f"{prefix} |-- name: {self.solver}")

  def exportToSSD(self, node):
    component_node = ET.SubElement(node, namespace.tag("ssd", "Component"))
    component_node.set("name", str(self.name))
    component_node.set("type", "application/x-fmu-sharedlibrary")
    component_node.set("source", str(self.fmuPath))
    if self.implementation == "me":
      component_node.set("implementation", "ModelExchange")
    elif self.implementation == "cs":
      component_node.set("implementation", "CoSimulation")
    else:
      component_node.set("implementation", "any")

    if self.description:
      component_node.set("description", self.description)

    if len(self.connectors) > 0:
      connectors_node = ET.SubElement(component_node, namespace.tag("ssd", "Connectors"))
      ## export component connectors
      for connector in self.connectors:
        connector.exportToSSD(connectors_node)

    ## export component element geometry
    if self.elementgeometry:
      self.elementgeometry.exportToSSD(component_node)

    ## export parameter bindings
    self.value.exportToSSD(component_node, self.parameterMapping, self.unitDefinitions)

    ## export parameters binding to ssd file with reference to ssv file
    if len(self.parameterResources) > 0:
      parameter_bindings_node = ET.SubElement(component_node, namespace.tag("ssd", "ParameterBindings"))
      for resource in self.parameterResources:
        for key, value in resource.items():
          parameter_binding_node = ET.SubElement(parameter_bindings_node, namespace.tag("ssd", "ParameterBinding"))
          parameter_binding_node.set("source", key)
          if value:
            parameter_mapping_node = ET.SubElement(parameter_binding_node, namespace.tag("ssd", "ParameterMapping"))
            parameter_mapping_node.set("source", value)

    ## export MetaData resources to ssd file
    if len(self.metaDataResources) > 0:
      for resource in self.metaDataResources:
        meta_data_node = ET.SubElement(component_node, namespace.tag("ssc", "MetaData"))
        meta_data_node.set("kind", resource.get("kind"))
        meta_data_node.set("type", resource.get("type"))
        meta_data_node.set("source", resource.get("source"))

    ## export Annotations
    if self.solver:
      from OMSimulator import utils
      utils.exportSimulationInformation(component_node, self.solver)

  def setValue(self, cref:str, value, unit=None, description = None):
    for connector in self.connectors:
      if str(connector.name) == str(cref):
        self.value.setValue(cref, value, connector.signal_type, unit, description)
        return
    ## it is possible that the parameter is not defined as connector but only in the ssv file or ssm mapping, so we allow setting values without types
    self.value.setValue(cref, value, None, unit, description)

  def getValue(self, cref: str):
    entry = self.value.getValue(cref)
    if entry is not None:
      return entry
    # Fall back to the default start value from modeldescription.xml.
    # modelDescriptionStartValue is a raw string parsed from XML — returned as-is;
    # the caller is responsible for converting to the target numeric type.
    if self.fmu is not None:
      for var in self.fmu.variables:
        if str(var.name) == str(cref):
          return var.modelDescriptionStartValue
    return None

  def setSolver(self, name: str):
    self.solver = name