# Copyright 2023 OpenC3, 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.

# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.

import json
from openc3.api import WHITELIST
from openc3.environment import OPENC3_SCOPE
from openc3.utilities.authorization import authorize
from openc3.topics.topic import Topic
from openc3.topics.interface_topic import InterfaceTopic
from openc3.topics.decom_interface_topic import DecomInterfaceTopic
from openc3.models.cvt_model import CvtModel
from openc3.models.target_model import TargetModel
from openc3.models.interface_model import InterfaceModel
from openc3.utilities.extract import *

WHITELIST.extend(
    [
        "tlm",
        "tlm_raw",
        "tlm_formatted",
        "tlm_with_units",
        "set_tlm",
        "inject_tlm",
        "override_tlm",
        "get_overrides",
        "normalize_tlm",
        "get_tlm_buffer",
        "get_tlm_packet",
        "get_tlm_values",
        "get_all_telemetry",
        "get_all_telemetry_names",
        "get_telemetry",
        "get_item",
        "subscribe_packets",
        "get_packets",
        "get_tlm_cnt",
        "get_tlm_cnts",
        "get_packet_derived_items",
    ]
)


# Request a telemetry item from a packet.
#
# Accepts two different calling styles:
#   tlm("TGT PKT ITEM")
#   tlm('TGT','PKT','ITEM')
#
# Favor the first syntax where possible as it is more succinct.
#
# @param args [String|Array<String>] See the description for calling style
# @param type [Symbol] Telemetry type, :RAW, :CONVERTED (default), :FORMATTED, or :WITH_UNITS
# @return [Object] The telemetry value formatted as requested
def tlm(*args, type="CONVERTED", cache_timeout=0.1, scope=OPENC3_SCOPE):
    target_name, packet_name, item_name = _tlm_process_args(
        args, "tlm", cache_timeout=cache_timeout, scope=scope
    )
    authorize(
        permission="tlm", target_name=target_name, packet_name=packet_name, scope=scope
    )
    return CvtModel.get_item(
        target_name, packet_name, item_name, type, cache_timeout, scope
    )


def tlm_raw(*args, cache_timeout=0.1, scope=OPENC3_SCOPE):
    return tlm(*args, type="RAW", cache_timeout=cache_timeout, scope=scope)


def tlm_formatted(*args, cache_timeout=0.1, scope=OPENC3_SCOPE):
    return tlm(*args, type="FORMATTED", cache_timeout=cache_timeout, scope=scope)


def tlm_with_units(*args, cache_timeout=0.1, scope=OPENC3_SCOPE):
    return tlm(*args, type="WITH_UNITS", cache_timeout=cache_timeout, scope=scope)


# Set a telemetry item in the current value table.
#
# Note: If this is done while OpenC3 is currently receiving telemetry,
# this value could get overwritten at any time. Thus this capability is
# best used for testing or for telemetry items that are not received
# regularly through the target interface.
#
# Accepts two different calling styles:
#   set_tlm("TGT PKT ITEM = 1.0")
#   set_tlm('TGT','PKT','ITEM', 10.0)
#
# Favor the first syntax where possible as it is more succinct.
#
# @param args [String|Array<String>] See the description for calling style
# @param type [Symbol] Telemetry type, :RAW, :CONVERTED (default), :FORMATTED, or :WITH_UNITS
def set_tlm(*args, type="CONVERTED", scope=OPENC3_SCOPE):
    target_name, packet_name, item_name, value = _set_tlm_process_args(
        args, "set_tlm", scope
    )
    authorize(
        permission="tlm_set",
        target_name=target_name,
        packet_name=packet_name,
        scope=scope,
    )
    CvtModel.set_item(target_name, packet_name, item_name, value, type, scope)


# Injects a packet into the system as if it was received from an interface
#
# @param target_name [String] Target name of the packet
# @param packet_name [String] Packet name of the packet
# @param item_hash [Hash] Hash of item_name and value for each item you want to change from the current value table
# @param type [Symbol] Telemetry type, :RAW, :CONVERTED (default), :FORMATTED, or :WITH_UNITS
def inject_tlm(
    target_name, packet_name, item_hash=None, type="CONVERTED", scope=OPENC3_SCOPE
):
    authorize(
        permission="tlm_set",
        target_name=target_name,
        packet_name=packet_name,
        scope=scope,
    )
    target_name = target_name.upper()
    packet_name = packet_name.upper()
    if type not in CvtModel.VALUE_TYPES:
        raise RuntimeError(f"Unknown type '{type}' for {target_name} {packet_name}")

    if item_hash:
        item_hash = {k.upper(): v for k, v in item_hash.items()}
        # Check that the items exist ... exceptions are raised if not
        TargetModel.packet_items(
            target_name, packet_name, item_hash.keys(), scope=scope
        )
    else:
        # Check that the packet exists ... exceptions are raised if not
        TargetModel.packet(target_name, packet_name, scope=scope)

    # See if this target has a tlm interface
    interface_name = None
    for _, interface in InterfaceModel.all(scope):
        if target_name in interface["tlm_target_names"]:
            interface_name = interface["name"]
            break

    # Use an interface microservice if it exists, other use the decom microservice
    if interface_name:
        InterfaceTopic.inject_tlm(
            interface_name, target_name, packet_name, item_hash, type=type, scope=scope
        )
    else:
        DecomInterfaceTopic.inject_tlm(
            target_name, packet_name, item_hash, type=type, scope=scope
        )


# Override the current value table such that a particular item always
# returns the same value (for a given type) even when new telemetry
# packets are received from the target.
#
# Accepts two different calling styles:
#   override_tlm("TGT PKT ITEM = 1.0")
#   override_tlm('TGT','PKT','ITEM', 10.0)
#
# Favor the first syntax where possible as it is more succinct.
#
# @param args The args must either be a string followed by a value or
#   three strings followed by a value (see the calling style in the
#   description).
# @param type [Symbol] Telemetry type, :ALL (default), :RAW, :CONVERTED, :FORMATTED, :WITH_UNITS
def override_tlm(*args, type="ALL", scope=OPENC3_SCOPE):
    target_name, packet_name, item_name, value = _set_tlm_process_args(
        args, "override_tlm", scope
    )
    authorize(
        permission="tlm_set",
        target_name=target_name,
        packet_name=packet_name,
        scope=scope,
    )
    CvtModel.override(
        target_name, packet_name, item_name, value, type=type, scope=scope
    )


# Get the list of CVT overrides
def get_overrides(scope=OPENC3_SCOPE):
    authorize(permission="tlm", scope=scope)
    return CvtModel.overrides(scope=scope)


# Normalize a telemetry item in a packet to its default behavior. Called
# after override_tlm to restore standard processing.
#
# Accepts two different calling styles:
#   normalize_tlm("TGT PKT ITEM")
#   normalize_tlm('TGT','PKT','ITEM')
#
# Favor the first syntax where possible as it is more succinct.
#
# @param args The args must either be a string or three strings
#   (see the calling style in the description).
# @param type [Symbol] Telemetry type, :ALL (default), :RAW, :CONVERTED, :FORMATTED, :WITH_UNITS
#   Also takes :ALL which means to normalize all telemetry types
def normalize_tlm(*args, type="ALL", scope=OPENC3_SCOPE):
    target_name, packet_name, item_name = _tlm_process_args(
        args, "normalize_tlm", scope=scope
    )
    authorize(
        permission="tlm_set",
        target_name=target_name,
        packet_name=packet_name,
        scope=scope,
    )
    CvtModel.normalize(target_name, packet_name, item_name, type=type, scope=scope)


# Returns the raw buffer for a telemetry packet.
#
# @param target_name [String] Name of the target
# @param packet_name [String] Name of the packet
# @return [Hash] telemetry hash with last telemetry buffer
def get_tlm_buffer(target_name, packet_name, scope=OPENC3_SCOPE):
    target_name = target_name.upper()
    packet_name = packet_name.upper()
    authorize(
        permission="tlm", target_name=target_name, packet_name=packet_name, scope=scope
    )
    TargetModel.packet(target_name, packet_name, scope=scope)
    topic = f"{scope}__TELEMETRY__{{{target_name}}}__{packet_name}"
    msg_id, msg_hash = Topic.get_newest_message(topic)
    if msg_id:
        # Decode the keys for user convenience
        return {k.decode(): v for (k, v) in msg_hash.items()}
    return None


# Returns all the values (along with their limits state) for a packet.
#
# @param target_name [String] Name of the target
# @param packet_name [String] Name of the packet
# @param stale_time [Integer] Time in seconds from Time.now that packet will be marked stale
# @param type [Symbol] Types returned, :RAW, :CONVERTED (default), :FORMATTED, or :WITH_UNITS
# @return [Array<String, Object, Symbol|None>] Returns an Array consisting
#   of [item name, item value, item limits state] where the item limits
#   state can be one of {OpenC3::Limits::LIMITS_STATES}
def get_tlm_packet(
    target_name, packet_name, stale_time=30, type="CONVERTED", scope=OPENC3_SCOPE
):
    target_name = target_name.upper()
    packet_name = packet_name.upper()
    authorize(
        permission="tlm", target_name=target_name, packet_name=packet_name, scope=scope
    )
    packet = TargetModel.packet(target_name, packet_name, scope=scope)
    t = _validate_tlm_type(type)
    if t is None:
        raise AttributeError(f"Unknown type '{type}' for {target_name} {packet_name}")
    cvt_items = [
        [target_name, packet_name, item["name"].upper(), type]
        for item in packet["items"]
    ]
    # This returns an array of arrays containin the value and the limits state:
    # [[0, None], [0, 'RED_LOW'], ... ]
    current_values = CvtModel.get_tlm_values(
        cvt_items, stale_time=stale_time, scope=scope
    )
    result = []
    # Combine the values with the item name
    for index, item in enumerate(current_values):
        result.append([cvt_items[index][2], item[0], item[1]])
    return result


# Returns all the item values (along with their limits state). The items
# can be from any target and packet and thus must be fully qualified with
# their target and packet names.
#
# @param items [Array<String>] Array of items consisting of 'tgt__pkt__item__type'
# @param stale_time [Integer] Time in seconds from Time.now that data will be marked stale
# @return [Array<Object, Symbol>]
#   Array consisting of the item value and limits state
#   given as symbols such as :RED, :YELLOW, :STALE
def get_tlm_values(items, stale_time=30, cache_timeout=0.1, scope=OPENC3_SCOPE):
    if type(items) != list or len(items) == 0 or type(items[0]) != str:
        raise AttributeError(
            "items must be array of strings: ['TGT__PKT__ITEM__TYPE', ...]"
        )
    packets = []
    cvt_items = []
    for item in items:
        try:
            target_name, packet_name, item_name, value_type = item.upper().split("__")
        except ValueError:
            raise AttributeError("items must be formatted as TGT__PKT__ITEM__TYPE")
        if packet_name == "LATEST":
            packet_name = CvtModel.determine_latest_packet_for_item(
                target_name, item_name, cache_timeout, scope
            )
        # Change packet_name in case of LATEST and ensure upcase
        cvt_items.append([target_name, packet_name, item_name, value_type])
        packets.append([target_name, packet_name])
    # Make the array of arrays unique
    packets = [list(x) for x in set(tuple(x) for x in packets)]
    for name in packets:
        authorize(
            permission="tlm",
            target_name=name[0],
            packet_name=name[1],
            scope=scope,
        )
    return CvtModel.get_tlm_values(cvt_items, stale_time, cache_timeout, scope)


# Returns an array of all the telemetry packet hashes
#
# @param target_name [String] Name of the target
# @return [Array<Hash>] Array of all telemetry packet hashes
def get_all_telemetry(target_name, scope=OPENC3_SCOPE):
    target_name = target_name.upper()
    authorize(permission="tlm", target_name=target_name, scope=scope)
    return TargetModel.packets(target_name, type="TLM", scope=scope)


# Returns an array of all the telemetry packet names
#
# @param target_name [String] Name of the target
# @return [Array<String>] Array of all telemetry packet names
def get_all_telemetry_names(target_name, scope=OPENC3_SCOPE):
    target_name = target_name.upper()
    authorize(permission="cmd_info", target_name=target_name, scope=scope)
    return TargetModel.packet_names(target_name, type="TLM", scope=scope)


# Returns a telemetry packet hash
#
# @param target_name [String] Name of the target
# @param packet_name [String] Name of the packet
# @return [Hash] Telemetry packet hash
def get_telemetry(target_name, packet_name, scope=OPENC3_SCOPE):
    target_name = target_name.upper()
    packet_name = packet_name.upper()
    authorize(
        permission="tlm", target_name=target_name, packet_name=packet_name, scope=scope
    )
    return TargetModel.packet(target_name, packet_name, scope=scope)


# Returns a telemetry packet item hash
#
# @param target_name [String] Name of the target
# @param packet_name [String] Name of the packet
# @param item_name [String] Name of the packet
# @return [Hash] Telemetry packet item hash
def get_item(target_name, packet_name, item_name, scope=OPENC3_SCOPE):
    target_name = target_name.upper()
    packet_name = packet_name.upper()
    item_name = item_name.upper()
    authorize(
        permission="tlm", target_name=target_name, packet_name=packet_name, scope=scope
    )
    return TargetModel.packet_item(target_name, packet_name, item_name, scope=scope)


# 2x double underscore since __ is reserved
SUBSCRIPTION_DELIMITER = "____"


# Subscribe to a list of packets. An ID is returned which is passed to
# get_packets(id) to return packets.
#
# @param packets [Array<Array<String, String>>] Array of arrays consisting of target name, packet name
# @return [String] ID which should be passed to get_packets
def subscribe_packets(packets, scope=OPENC3_SCOPE):
    if type(packets) is not list or type(packets[0]) is not list:
        raise RuntimeError("packets must be nested array: [['TGT','PKT'],...]")

    result = {}
    for target_name, packet_name in packets:
        target_name = target_name.upper()
        packet_name = packet_name.upper()
        authorize(
            permission="tlm",
            target_name=target_name,
            packet_name=packet_name,
            scope=scope,
        )
        topic = f"{scope}__DECOM__{{{target_name}}}__{packet_name}"
        id, _ = Topic.get_newest_message(topic)

        if id:
            result[topic] = id
        else:
            result[topic] = "0-0"
    mylist = []
    for k, v in result.items():
        mylist += [k, v]
    return SUBSCRIPTION_DELIMITER.join(mylist)


# Get packets based on ID returned from subscribe_packet.
# @param id [String] ID returned from subscribe_packets or last call to get_packets
# @param count [Integer] Maximum number of packets to return from EACH packet stream
# @return [Array<String, Array<Hash>] Array of the ID and array of all packets found
def get_packets(id, count=1000, scope=OPENC3_SCOPE):
    authorize(permission="tlm", scope=scope)
    # Split the list of topic, ID values and turn it into a hash for easy updates
    items = id.split(SUBSCRIPTION_DELIMITER)
    # Convert it back into a dict to create a lookup
    lookup = dict(zip(items[::2], items[1::2]))
    packets = []
    for topic, _, msg_hash, _ in Topic.read_topics(
        lookup.keys(), list(lookup.values()), None, count
    ):
        # # Return the original ID and and empty array if we didn't get anything
        # for topic, data in xread:
        # for id, msg_hash in data:
        lookup[topic] = id  # save the new ID
        # decode the binary string keys and values to strings
        msg_hash = {k.decode(): v.decode() for (k, v) in msg_hash.items()}
        json_hash = json.loads(msg_hash["json_data"])
        msg_hash.pop("json_data")
        packets.append(msg_hash | json_hash)
    mylist = []
    for k, v in lookup.items():
        mylist += [k, v]
    return (SUBSCRIPTION_DELIMITER.join(mylist), packets)


# Get the receive count for a telemetry packet
#
# @param target_name [String] Name of the target
# @param packet_name [String] Name of the packet
# @return [Numeric] Receive count for the telemetry packet
def get_tlm_cnt(target_name, packet_name, scope=OPENC3_SCOPE):
    target_name = target_name.upper()
    packet_name = packet_name.upper()
    authorize(
        permission="system",
        target_name=target_name,
        packet_name=packet_name,
        scope=scope,
    )
    TargetModel.packet(target_name, packet_name, scope=scope)
    return Topic.get_cnt(f"{scope}__TELEMETRY__{{{target_name}}}__{packet_name}")


# Get the transmit counts for telemetry packets
#
# @param target_packets [Array<Array<String, String>>] Array of arrays containing target_name, packet_name
# @return [Numeric] Transmit count for the command
def get_tlm_cnts(target_packets, scope=OPENC3_SCOPE):
    authorize(permission="system", scope=scope)
    counts = []
    for target_name, packet_name in target_packets:
        target_name = target_name.upper()
        packet_name = packet_name.upper()
        counts.append(
            Topic.get_cnt(f"{scope}__TELEMETRY__{{{target_name}}}__{packet_name}")
        )
    return counts


# Get the list of derived telemetry items for a packet
#
# @param target_name [String] Target name
# @param packet_name [String] Packet name
# @return [Array<String>] All of the ignored telemetry items for a packet.
def get_packet_derived_items(target_name, packet_name, scope=OPENC3_SCOPE):
    target_name = target_name.upper()
    packet_name = packet_name.upper()
    authorize(
        permission="tlm", target_name=target_name, packet_name=packet_name, scope=scope
    )
    packet = TargetModel.packet(target_name, packet_name, scope=scope)
    return [item["name"] for item in packet["items"] if item["data_type"] == "DERIVED"]


def _validate_tlm_type(type):
    match type:
        case "RAW":
            return ""
        case "CONVERTED":
            return "C"
        case "FORMATTED":
            return "F"
        case "WITH_UNITS":
            return "U"
    return None


def _tlm_process_args(args, method_name, cache_timeout=0.1, scope=OPENC3_SCOPE):
    match (len(args)):
        case 1:
            target_name, packet_name, item_name = extract_fields_from_tlm_text(args[0])
        case 3:
            target_name = args[0]
            packet_name = args[1]
            item_name = args[2]
        case _:
            # Invalid number of arguments
            raise RuntimeError(
                f"ERROR: Invalid number of arguments ({len(args)}) passed to {method_name}()"
            )
    target_name = target_name.upper()
    packet_name = packet_name.upper()
    item_name = item_name.upper()
    if packet_name == "LATEST":
        packet_name = CvtModel.determine_latest_packet_for_item(
            target_name, item_name, cache_timeout, scope
        )
    else:
        # Determine if this item exists, it will raise appropriate errors if not
        TargetModel.packet_item(target_name, packet_name, item_name, scope=scope)
    return target_name, packet_name, item_name


def _set_tlm_process_args(args, method_name, scope=OPENC3_SCOPE):
    match len(args):
        case 1:
            (
                target_name,
                packet_name,
                item_name,
                value,
            ) = extract_fields_from_set_tlm_text(args[0])
        case 4:
            target_name = args[0]
            packet_name = args[1]
            item_name = args[2]
            value = args[3]
        case _:
            # Invalid number of arguments
            raise RuntimeError(
                f"ERROR: Invalid number of arguments ({len(args)}) passed to {method_name}()"
            )
    target_name = target_name.upper()
    packet_name = packet_name.upper()
    item_name = item_name.upper()
    # Determine if this item exists, it will raise appropriate errors if not
    TargetModel.packet_item(target_name, packet_name, item_name, scope=scope)

    return target_name, packet_name, item_name, value
