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.implementation = implementation

  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 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 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 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 Annotations
    if self.solver:
      from OMSimulator import utils
      utils.exportAnnotations(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):
    return self.value.getValue(cref)

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