import logging

from enum import Enum
from typing import Optional, List, Union
from pydantic import BaseModel, Field, validator, UUID4, PositiveInt

from .ethercat import *
from .profinet import *
from .ethernetip import *
from .modbus import *

_logger = logging.getLogger(__name__)

HelpText = {
    "Root":{
        "name": "The name of the model. Typically the same as the device name for a single device design.",
        "vendor": "Name of company producing the device. Shall match Profinet and EtherCAT Vendor IDs",
        "adapters": "Enabled fieldbus adapters",
        "devices": "List of devices. Often a single device.",
        "modules": "List of modules. A module is a defines a set of inputs, outputs and more. A module can be instantiated in several slots  ",
        "ethercat": "EtherCAT specific configuration",
        "profinet": "Profinet specific configuration",
        "ethernetip": "EtherNet/IP specific configuration",
        "modbus": "Modbus specific configuration"
    },
    "Device": {
        "name": "The name of the device. Use alphanumeric characters and space to avoid problems.",
        "description": "Short description of functionality. The information will go into device description files and be visible in engineering tools.",
        "hardware_release": "Hardware version string",
        "software_release": "Software version string",
        "serial": "Default serial number as a string. Currently ignored.",
        "loglevel": "Core log level. Currently ignored.",
        "webgui_enable": "Enable device web server. Currently ignored. Webserver is always running. ",
        "slots": "List of slots. Each containing a module.",
        "ethercat": "Device specific EtherCAT configuration",
        "profinet": "Device specific Profinet configuration",
        "ethernetip":"Device specific EtherNet/IP configuration",
        "modbus": "Device specific Modbus configuration",
        "nbr_of_ports": "Number of Ethernet ports on the device"
    },
    "Module": {
        "name": "The name of the module. Will go into device description files and be visible in engineering tools. ",
        "description": "Short description of module functionality. The information will go into device description files and be visible in engineering tools.",
        "inputs": "List of input signals. Input data is generated by device and sent to controller.",
        "outputs": "List of output signals. Output data is generated by controller and received by device",
        "parameters": "List of configuration parameters. Parameters are not part of cyclic process data. Typically set by PLC during startup. ",
        "alarms": "List of supported alarms. Alarms are set by the device to notify the controller on important events",
        "ethercat": "Device specific EtherCAT configuration",
        "profinet": "Module specific Profinet configuration"
    },
    "Slot": {
        "name": "Slot name. U-Phy specific. Not visible in engineering tools",
        "module": "Which module to use in this slot"
    },
    "Alarm": {
        "name": "The alarm name, used for code generation. Not visible in engineering tools",
        "message": "Message shown in PLC applications / SCADA systems" ,
        "error_code": "A number identifying the alarm"
    },
    "BaseSignal": {
        "name": "Signal name. Will be visible in engineering tools.",
        "description": "Short description that will be visible in engineering tools ",
        "datatype": "Signal data type",
        "derived": "Currently not used"
    },
    "Parameter": {
        "default": "Default value",
        "min": "Min value",
        "max": "Max value",
        "persistent": "True if value is stored in non volatile memory",
        "permissions": "Read/Write permissions",
        "profinet": " Profinet parameter index"
    },
    "Signal": {
        "is_array": "True if signal is an array of values",
        "array_length": "Length of signal array",
    }
}

class LogLevel(str, Enum):
    FATAL = 'FATAL'
    ERROR = 'ERROR'
    WARNING = 'WARNING'
    INFO = 'INFO'
    DEBUG = 'DEBUG'

    def __str__(self):
        return self.value


class DataType(str, Enum):
    """Datatypes"""
    UINT32 = 'UINT32'
    UINT16 = 'UINT16'
    UINT8 = 'UINT8'
    INT32 = 'INT32'
    INT16 = 'INT16'
    INT8 = 'INT8'
    REAL32 = 'REAL32'

    def __str__(self):
        return self.value

    @property
    def bitlen(self):
        map = {
            DataType.UINT32: 32,
            DataType.UINT16: 16,
            DataType.UINT8: 8,
            DataType.INT32: 32,
            DataType.INT16: 16,
            DataType.INT8: 8,
            DataType.REAL32: 32,
        }
        return map[self.value]


class Permissions(str, Enum):
    """Read/write permissions"""
    RO = 'RO'
    RW = 'RW'
    WO = 'WO'
    RWpre = 'RWpre'  # Dynamic permission (depends on state)

class Adapter(str, Enum):
    """Available fieldbus adapters"""
    PROFINET = 'Profinet'
    ETHERCAT = 'EtherCAT'
    ETHERNETIP = 'EtherNet/IP'
    MODBUS = 'Modbus'

class ModelElement(BaseModel):
    """Base Class elements with unique id"""
    id: UUID4

class BaseSignal(ModelElement):
    """Base signal defines properties common to signals containing cyclic data and parameters """
    name: str = Field(title="Signal name", description=HelpText['BaseSignal']['name'])
    description: str = Field(title="Description", default="", description=HelpText['BaseSignal']['description'])
    datatype: DataType = Field(title="Signal datatype", description=HelpText['BaseSignal']['datatype'])
    #derived: bool = Field(title="Derived", default=False, description=HelpText['BaseSignal']['derived'])

    @property
    def bitlen(self):
        return self.datatype.bitlen

class Signal(BaseSignal):
    """Signals are cyclic data. They can for instance model a sensor
    or actuator."""
    is_array: Optional[bool] = Field(title="Array", default=False, description=HelpText['Signal']['is_array'])
    array_length: Optional[PositiveInt] = Field(title="Array Length ", description=HelpText['Signal']['array_length'])

    @property
    def bitlen(self):
        if not self.is_array:
            return self.datatype.bitlen
        else:
           return self.datatype.bitlen * self.array_length


class Parameter(BaseSignal):
    """Parameters are acyclic data. They usually model configuration objects."""
    default: str = Field(title="Default value", default="0", description=HelpText['Parameter']['default'])
    min: Optional[str] = Field(title="Min value", description=HelpText['Parameter']['min'])
    max: Optional[str] = Field(title="Max value", description=HelpText['Parameter']['max'])
    #persistent: bool = Field(title="Persistent", default=False, description=HelpText['Parameter']['persistent'])
    permissions: Permissions = Field(title="Permissions", default=Permissions.RW, description=HelpText['Parameter']['permissions'])
    profinet: Optional[ProfinetParameter] = Field(title="Profinet parameter", description=HelpText['Parameter']['profinet'])


class Alarm(ModelElement):
    name: str = Field(title="Alarm name", description=HelpText['Alarm']['name'])
    message: str = Field(title="Alarm message",description=HelpText['Alarm']['message'])
    error_code: str = Field(title="Error code", description=HelpText['Alarm']['error_code'])


class Module(ModelElement):
    """A module is a reusable component that can be instantiated in a slot in a device."""
    name: str = Field(title="Module name", description=HelpText['Module']['name'])
    description: str = Field(title="Description", default="", description=HelpText['Module']['description'])
    inputs: List[Signal] = Field(title="Inputs", default=[], description=HelpText['Module']['inputs'])
    outputs: List[Signal] = Field(title="Outputs", default=[], description=HelpText['Module']['outputs'])
    parameters: List[Parameter] = Field(title="Parameters", default=[], description=HelpText['Module']['parameters'])
    alarms: List[Alarm] = Field(title="Alarm", default=[], description=HelpText['Module']['alarms'])
    ethercat: EtherCATModule = Field(title="EtherCAT module", default=EtherCATModule(), description=HelpText['Module']['ethercat'])
    profinet: ProfinetModule = Field(title="Profinet module", default=ProfinetModule(), description=HelpText['Module']['profinet'])

    def get_signal(self, signals, id):
        return [s for s in signals if s.id == id][0]

    def get_offset(self, signals, s):
        ix = signals.index(s)
        offset = sum([s.bitlen for s in signals[0:ix]])
        return offset

    def get_bitlen(self, signals):
        return sum([s.bitlen for s in signals])

    @property
    def inputs_bitlen(self):
        return self.get_bitlen(self.inputs)

    @property
    def outputs_bitlen(self):
        return self.get_bitlen(self.outputs)

    @property
    def parameters_bitlen(self):
        return self.get_bitlen(self.parameters)


class Slot(BaseModel):
    """A device consists of one or more slots. Each slot contains a module."""
    name: str = Field(title="Slot name", description=HelpText['Slot']['name'])
    module: UUID4 = Field(title="Module", description=HelpText['Slot']['module'])


class Device(ModelElement):
    """A device models a fieldbus device. It consists of one or more
    slots. Each slot contains a module."""
    name: str = Field(title="Device name", description=HelpText['Device']['name'])
    description: str = Field(title="Description", default="", description=HelpText['Device']['description'])
    hardware_release: str = Field(title="Hardware release", default="V0.1.0" , description=HelpText['Device']['hardware_release'])
    software_release: str = Field(title="Software release", default="V0.1.0", description=HelpText['Device']['software_release'])
    serial: Optional[str] = Field(title="Serial number", description=HelpText['Device']['serial'])
    loglevel: LogLevel = Field(title="U-Phy logging level", default=LogLevel.INFO, description=HelpText['Device']['loglevel'])
    webgui_enable: bool = Field(title="Enable webserver", default=True, description=HelpText['Device']['webgui_enable'])
    slots: List[Slot] = Field(title="Slots", description=HelpText['Device']['slots'])
    ethercat: Optional[EtherCATDevice] = Field(title="EtherCAT device", description=HelpText['Device']['ethercat'])
    profinet: Optional[ProfinetDevice] = Field(title="Profinet device", description=HelpText['Device']['profinet'])
    ethernetip: Optional[EthernetipDevice] = Field(title="EtherNet/IP device", description=HelpText['Device']['ethernetip'])
    modbus: Optional[ModbusDevice] = Field(title="Modbus device", description=HelpText['Device']['modbus'])
    nbr_of_ports: Optional[int] = Field(title="Number of ports", default=2, description=HelpText['Device']['nbr_of_ports'])

    def get_used_module_index(self, id, model):
        """Get index in modules list from module id"""
        m = model.get_module(id)
        return self.get_used_modules(model).index(m)

    def get_used_modules(self, model):
        """Get list of modules plugged into slots"""
        used_modules = []
        for s in self.slots:
            if model.get_module(s.module) not in used_modules:
                used_modules.append(model.get_module(s.module))
        return used_modules

    def has_alarms(self, model):
        for m in self.get_used_modules(model):
            if len(m.alarms) > 0:
                return True
        return False

    def get_inputs_frame_size(self, model):
       """Get input frame size in number of bytes"""
       bitlen = 0
       for s in self.slots:
           bitlen += model.get_module(s.module).inputs_bitlen
       return (bitlen + 7) // 8

    def get_outputs_frame_size(self, model):
       """Get output frame size in number of bytes"""
       bitlen = 0
       for s in self.slots:
           bitlen += model.get_module(s.module).outputs_bitlen
       return (bitlen + 7) // 8

class Root(BaseModel):
    """The U-Phy data model is used to define fieldbus devices. A
    device contains one or more slots. Each slot contains one
    module. A module is a reusable component that can be instantiated
    in more than one slot."""
    name: str = Field(title="Model name", description=HelpText['Root']['name'])
    vendor: str = Field(title="Vendor name", description=HelpText['Root']['vendor'])
    adapters: List[Adapter] = Field(
        title="Adapters",
        default=[
            Adapter.PROFINET,
            Adapter.ETHERCAT,
            Adapter.ETHERNETIP,
            Adapter.MODBUS
        ],
        min_items=1,
        uniqueItems=True,
        description=HelpText['Root']['adapters']
    )
    devices: List[Device] = Field(title="Devices", default=[], description=HelpText['Root']['devices'])
    modules: List[Module] = Field(title="Modules", default=[], description=HelpText['Root']['modules'])
    ethercat: Optional[EtherCATRoot] = Field(title="EtherCAT", description=HelpText['Root']['ethercat'])
    profinet: Optional[ProfinetRoot] = Field(title="Profinet",description=HelpText['Root']['profinet'])
    ethernetip: Optional[EthernetipRoot] = Field(title="EtherNet/IP",description=HelpText['Root']['ethernetip'])
    modbus: Optional[ModbusRoot] = Field(title="Modbus",description=HelpText['Root']['modbus'])

    def get_device(self, name):
        return [device for device in self.devices if device.name == name][0]

    def get_module(self, id):
        return [m for m in self.modules if m.id == id][0]

    def get_module_index(self, id):
        """Get index in modules list from module id"""
        m = self.get_module(id)
        return self.modules.index(m)

    @classmethod
    def parse_obj(cls, obj):
        model = super().parse_obj(obj)
        for ix, m in enumerate(model.modules):
            m.ethercat.generate_mapping(m, ix)
            m.profinet.generate_mapping(m, ix)

        return model

    class Config:
        title = 'U-Phy'
