# See LICENSE for licensing information.
#
# Copyright (c) 2016-2023 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
from abc import ABC, abstractmethod
from openram import debug
from openram.tech import drc, parameter, spice
from .stimuli import *
from .charutils import *


class spice_measurement(ABC):
    """Base class for spice stimulus measurements."""
    def __init__(self, measure_name, measure_scale=None, has_port=True):
        #Names must be unique for correct spice simulation, but not enforced here.
        self.name = measure_name
        self.measure_scale = measure_scale
        self.has_port = has_port #Needed for error checking
        #Some meta values used externally. variables are added here for consistency accross the objects
        self.meta_str = None
        self.meta_add_delay = False
    @abstractmethod
    def get_measure_function(self):
        return None

    @abstractmethod
    def get_measure_values(self):
        return None

    def write_measure(self, stim_obj, input_tuple):
        measure_func = self.get_measure_function()
        if measure_func == None:
            debug.error("Did not set measure function",1)
        measure_vals = self.get_measure_values(*input_tuple)
        measure_func(stim_obj, *measure_vals)

    def retrieve_measure(self, port=None):
        self.port_error_check(port)
        if port != None:
            value = parse_spice_list("timing", "{0}{1}".format(self.name.lower(), port))
        else:
            value = parse_spice_list("timing", "{0}".format(self.name.lower()))
        if type(value)!=float or self.measure_scale == None:
            return value
        else:
            return value*self.measure_scale

    def port_error_check(self, port):
        if self.has_port and port == None:
            debug.error("Cannot retrieve measurement, port input was expected.",1)
        elif not self.has_port and port != None:
            debug.error("Unexpected port input received during measure retrieval.",1)


class delay_measure(spice_measurement):
    """Generates a spice measurement for the delay of 50%-to-50% points of two signals."""

    def __init__(self,
                 measure_name,
                 trig_name,
                 targ_name,
                 trig_dir_str,
                 targ_dir_str,
                 trig_vdd=0.5,
                 targ_vdd=0.5,
                 measure_scale=None,
                 has_port=True):
        spice_measurement.__init__(self, measure_name, measure_scale, has_port)
        self.set_meas_constants(trig_name, targ_name, trig_dir_str, targ_dir_str, trig_vdd, targ_vdd)

    def get_measure_function(self):
        return stimuli.gen_meas_delay

    def set_meas_constants(self, trig_name, targ_name, trig_dir_str, targ_dir_str, trig_vdd, targ_vdd):
        """Set the constants for this measurement: signal names, directions, and trigger scales"""
        self.trig_dir_str = trig_dir_str
        self.targ_dir_str = targ_dir_str
        self.trig_val_of_vdd = trig_vdd
        self.targ_val_of_vdd = targ_vdd
        self.trig_name_no_port = trig_name
        self.targ_name_no_port = targ_name

        # Time delays and ports are variant and needed as inputs when writing the measurement

    def get_measure_values(self, trig_td, targ_td, vdd_voltage, port=None):
        """Constructs inputs to stimulus measurement function. Variant values are inputs here."""
        self.port_error_check(port)
        trig_val = self.trig_val_of_vdd * vdd_voltage
        targ_val = self.targ_val_of_vdd * vdd_voltage

        if port != None:
            # For dictionary indexing reasons, the name is formatted differently than the signals
            meas_name = "{}{}".format(self.name, port)
            trig_name = self.trig_name_no_port.format(port)
            targ_name = self.targ_name_no_port.format(port)
        else:
            meas_name = self.name
            trig_name = self.trig_name_no_port
            targ_name = self.targ_name_no_port
        return (meas_name, trig_name, targ_name, trig_val, targ_val, self.trig_dir_str, self.targ_dir_str, trig_td, targ_td)


class slew_measure(delay_measure):

    def __init__(self, measure_name, signal_name, slew_dir_str, measure_scale=None, has_port=True):
        spice_measurement.__init__(self, measure_name, measure_scale, has_port)
        self.set_meas_constants(signal_name, slew_dir_str)

    def set_meas_constants(self, signal_name, slew_dir_str):
        """Set the values needed to generate a Spice measurement statement based on the name of the measurement."""
        self.trig_dir_str = slew_dir_str
        self.targ_dir_str = slew_dir_str

        if slew_dir_str == "RISE":
            self.trig_val_of_vdd = 0.1
            self.targ_val_of_vdd = 0.9
        elif slew_dir_str == "FALL":
            self.trig_val_of_vdd = 0.9
            self.targ_val_of_vdd = 0.1
        else:
            debug.error("Unrecognised slew measurement direction={}".format(slew_dir_str),1)
        self.trig_name_no_port = signal_name
        self.targ_name_no_port = signal_name

        # Time delays and ports are variant and needed as inputs when writing the measurement


class power_measure(spice_measurement):
    """Generates a spice measurement for the average power between two time points."""

    def __init__(self, measure_name, power_type="", measure_scale=None, has_port=True):
        spice_measurement.__init__(self, measure_name, measure_scale, has_port)
        self.set_meas_constants(power_type)

    def get_measure_function(self):
        return stimuli.gen_meas_power

    def set_meas_constants(self, power_type):
        """Sets values useful for power simulations. This value is only meta related to the lib file (rise/fall)"""
        # Not needed for power simulation
        self.power_type = power_type # Expected to be  "RISE"/"FALL"

    def get_measure_values(self, t_initial, t_final, port=None):
        """Constructs inputs to stimulus measurement function. Variant values are inputs here."""
        self.port_error_check(port)
        if port != None:
            meas_name = "{}{}".format(self.name, port)
        else:
            meas_name = self.name
        return (meas_name, t_initial, t_final)


class voltage_when_measure(spice_measurement):
    """Generates a spice measurement to measure the voltage of a signal based on the voltage of another."""

    def __init__(self, measure_name, trig_name, targ_name, trig_dir_str, trig_vdd, measure_scale=None, has_port=True):
        spice_measurement.__init__(self, measure_name, measure_scale, has_port)
        self.set_meas_constants(trig_name, targ_name, trig_dir_str, trig_vdd)

    def get_measure_function(self):
        return stimuli.gen_meas_find_voltage

    def set_meas_constants(self, trig_name, targ_name, trig_dir_str, trig_vdd):
        """Sets values useful for power simulations. This value is only meta related to the lib file (rise/fall)"""
        self.trig_dir_str = trig_dir_str
        self.trig_val_of_vdd = trig_vdd
        self.trig_name_no_port = trig_name
        self.targ_name_no_port = targ_name

    def get_measure_values(self, trig_td, vdd_voltage, port=None):
        """Constructs inputs to stimulus measurement function. Variant values are inputs here."""
        self.port_error_check(port)
        if port != None:
            # For dictionary indexing reasons, the name is formatted differently than the signals
            meas_name = "{}{}".format(self.name, port)
            trig_name = self.trig_name_no_port.format(port)
            targ_name = self.targ_name_no_port.format(port)
        else:
            meas_name = self.name
            trig_name = self.trig_name_no_port
            targ_name = self.targ_name_no_port
        trig_voltage = self.trig_val_of_vdd * vdd_voltage
        return (meas_name, trig_name, targ_name, trig_voltage, self.trig_dir_str, trig_td)


class voltage_at_measure(spice_measurement):
    """Generates a spice measurement to measure the voltage at a specific time.
       The time is considered variant with different periods."""

    def __init__(self, measure_name, targ_name, measure_scale=None, has_port=True):
        spice_measurement.__init__(self, measure_name, measure_scale, has_port)
        self.set_meas_constants(targ_name)

    def get_measure_function(self):
        return stimuli.gen_meas_find_voltage_at_time

    def set_meas_constants(self, targ_name):
        """Sets values useful for power simulations. This value is only meta related to the lib file (rise/fall)"""
        self.targ_name_no_port = targ_name

    def get_measure_values(self, time_at, port=None):
        """Constructs inputs to stimulus measurement function. Variant values are inputs here."""
        self.port_error_check(port)
        if port != None:
            # For dictionary indexing reasons, the name is formatted differently than the signals
            meas_name = "{}{}".format(self.name, port)
            targ_name = self.targ_name_no_port.format(port)
        else:
            meas_name = self.name
            targ_name = self.targ_name_no_port
        return (meas_name, targ_name, time_at)
