"""
    Copyright 2022 bitValence, Inc.
    All Rights Reserved.

    This program is free software; you can modify and/or redistribute it
    under the terms of the GNU Affero General Public License
    as published by the Free Software Foundation; version 3 with
    attribution addendums as found in the LICENSE.txt.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    Purpose:
    
        Core Flight Executive (cFE) Electronic Data Sheet (EDS) database interface
        classes. EDSs are used to define an application's interface.  The cfe-eds
        build toolchain produces artifacts that can be used by flight and ground
        software that needs to interface with an application. The following python
        binding files are created:

          EdsLib        - Contains generic EDS structure definitions
          CFE_MissioLib - Contains mission-specific definitions. The top-level
                          structure is a list of application 'instances'

        The following environment variables must be defined to use the EDS python
        libraries:

            LD_LIBRARY_PATH - TODO
            PYTHONPATH      - TODO

        The EDS and the python classes in this file use the following
        concepts/definitions:

            Mission     - The name of a mission that can include mutliple cFS
                          deployments
            cFS Target  - A single cFS deployment on a processor. The EDS supports
                          application "instances" and in theory multiple instances
                          could be deployed on a single cFS target. However the
                          current cFE release Caelum does not support this model.

"""

from abc import ABC, abstractmethod
import sys
import importlib
import time
import logging
logger = logging.getLogger(__name__)

import EdsLib
import CFE_MissionLib

###############################################################################

class EdsMission:
    """
    Class for interacting with the EDS libraries generated by the EDS build
    toolchain. This class should not be used as a base class because the EDS
    libraries should not be extended beynd what the build toolchain generates.
    Objects of this class should be owned by classes like CfeTarget. 
    """
    
    #todo: Are there invalid ID conventions in the EDS to use as default values & for validity checks
    NULL_ID  = -1
    NULL_CMD_STR = '--'        # Command parameter default value, keep short to simplify user entry
    NULL_TLM_STR = '--Null--'  # Display when no value received yet
    
    #todo: Decide how to manage these. They are a subset of 'standard' definitions
    # Supported interface types defined in the EDS
    TELECOMMAND_IF = "CFE_SB/Telecommand"
    TELEMETRY_IF   = "CFE_SB/Telemetry"

    APP_CMD_TOPIC_SUFFIX = '/Application/CMD'

    # Dictionary keys that can be used to indicate a selection has not been made
    TARGET_TITLE_KEY    = "-- Target --"
    TOPIC_CMD_TITLE_KEY = "-- Command Topic --"
    TOPIC_TLM_TITLE_KEY = "-- Telemetry Topic --"
    COMMAND_TITLE_KEY   = "-- Command --"

    def __init__(self, mission_name, interface_type):
        self.mission_name = mission_name
        self.interface_type = interface_type
        self.load_eds_database()

    def reload_libs(self):
        print('reload_libs')
        #importlib.invalidate_caches()
        #importlib.reload(EdsLib)
        #importlib.reload(CFE_MissionLib)
        for m in ['EdsLib', 'CFE_MissionLib']:
            print('m=',m)
            if m in sys.modules:
                del(sys.modules[m])
                print('reload_libs deleted ',m)

    def load_eds_database(self):
        try:
            self.lib_db     = EdsLib.Database(self.mission_name)
            self.cfe_db     = CFE_MissionLib.Database(self.mission_name, self.lib_db)  #cfe_db => CFE_MissionLib.Database(self.mission_name), type => CFE_MissionLib.Database
            self.interface  = self.cfe_db.Interface(self.interface_type)
        except RuntimeError:
            print("Error accessing EDS libraries. Verify your LD_LIBRARY_PATH, PYTHONPATH environment variable settings and mission name")
            logger.error("Error accessing EDS libraries. Verify your LD_LIBRARY_PATH, PYTHONPATH environment variable settings and mission name")

    
    
    def get_target_dict(self):
        """
        Returns the target dictionary based on the targets in the interface database
        target[0] = Name (string)
        target[1] = ID (integer)
        """
        target_dict = { EdsMission.TARGET_TITLE_KEY: EdsMission.NULL_ID }
        for target in self.cfe_db:
            target_dict[target[0]] = target[1]
        return target_dict
    
    
    def has_target(self, target_name):
        """
        Verify target exists and return target ID if it does
        """
          
        has_target = False
        target_id = EdsMission.NULL_ID
        
        target_dict = self.get_target_dict()
        
        if target_name in target_dict.keys():
           target_id = target_dict[target_name]
           has_target = True
           
        return (has_target, target_id) 
    
    
    def get_topic_dict(self):
        """
        Return a dictionary of topics for the interface. Topic names are used to 
        lookup topic IDs
        Telecommand topic = (topic name, ID) => ('CFE_ES/Application/CMD', 1)
        Telemetry topic   = (topic name, ID) => ('CFE_ES/Application/HK_TLM', 61)
        """
        if self.interface_type == EdsMission.TELECOMMAND_IF:
            topic_dict = {EdsMission.TOPIC_CMD_TITLE_KEY: EdsMission.NULL_ID}
        else:
            topic_dict = {EdsMission.TOPIC_TLM_TITLE_KEY: EdsMission.NULL_ID}

        for topic in self.interface:
            topic_dict[topic[0]] = topic[1]
        
        return topic_dict


    def get_eds_id_from_topic(self, topic_name):
        """
        Returns the EdsId associated with a given topic name
        """
        topic_obj = self.interface.Topic(topic_name)

        return topic_obj.EdsId


    def get_database_entry(self, eds_id):
        """
        Returns the EdsLib database entry for the given ID
        """
        return EdsLib.DatabaseEntry(self.lib_db, eds_id)


    def get_topic_payload(self, topic_name):
        eds_id = self.get_eds_id_from_topic(topic_name)
        return self.get_database_entry(eds_id)()['Payload']


    def get_database_named_entry(self, eds_name):
        """
        Returns the EdsLib database entry for the given name
        """
        logger.debug("get_database_named_entry() - %s" % eds_name)
        return EdsLib.DatabaseEntry(self.lib_db, eds_name)

  
    def get_packed_obj(self, eds_obj):
        """
        Returns a binary byte array for the given object
        """
        return EdsLib.PackedObject(eds_obj)

        
    def decode_message(self, raw_message):
        """
        Decodes a raw input message into an EdsObject

        Inputs:
        mission - User specified mission name
        intf_db - CFE_MissionLib Interface Database
        raw_message - Packed Bytes message

        Outputs:
        eds_entry - The EdsDb function to create the EDS object associated with the input message
        eds_object - The Unpacked EdsDb Object
        """
        eds_id, topic_id = self.cfe_db.DecodeEdsId(raw_message)
        eds_entry = EdsLib.DatabaseEntry(self.mission_name, eds_id)
        eds_object = eds_entry(EdsLib.PackedObject(raw_message))
        return (eds_entry, eds_object)

        
###############################################################################

class CfeEdsTarget:
    """
    Abstract base class for managing a cFE 'Target'. See file prologue for
    'Target' vs 'Instance'.
    """

    def __init__(self, mission_name, target_name, interface):
            
        self.mission_name = mission_name
        self.eds_mission  = EdsMission(mission_name, interface)
        self.target_name  = target_name

        self.reload_edS_database(False)
 
     
    def reload_edS_database(self, load_database=True):
        if load_database:
            self.eds_mission.load_eds_database()

        self.topic_dict = {}
        self.topic_id   = EdsMission.NULL_ID
        
        self.valid, self.id = self.eds_mission.has_target(self.target_name)        
        if self.valid:
            self.topic_dict = self.eds_mission.get_topic_dict()
        else:
            #todo: Invalid target error case. Can python constructor return a value?
            raise RuntimeError
    
    
    def get_topics(self):
        return self.topic_dict

        
    @abstractmethod
    def send_command(self, cmd_obj):
        raise NotImplementedError

        
    @abstractmethod        
    def recv_telemetry(self, process_tlm_msg):
        raise NotImplementedError



