import sys
import sqlite3
from memUtil import *
from math import *

metrics = []
threadMetrics = []


def metric(function):
    metrics.append(function)
    return function


def threadMetric(function):
    threadMetrics.append(function)
    return function


def getThreads(connection):
    cthread = getControllerThread(connection)
    cursor = connection.cursor()
    query = "SELECT DISTINCT(TThread) FROM transactions WHERE TThread != " + str(cthread) + " ORDER BY TThread"
    cursor.execute(query)
    result = []
    for currentRow in cursor:
        result.append(currentRow[0])
    return result


@metric
def trace_length_in_ns(connection):
    cursor = connection.cursor()
    cursor.execute(""" SELECT max(PhaseEnd)/1000 FROM PHASES; """)
    result = cursor.fetchone()
    return result[0]


@metric
def average_response_latency_in_ns(connection):
    cursor = connection.cursor()
    cursor.execute("""SELECT AVG(RESP.PHASEBEGIN -  REQ.PHASEBEGIN)/1000 FROM PHASES REQ, PHASES RESP WHERE REQ.PHASENAME = 'REQ' AND RESP.PHASENAME='RESP' AND REQ.TRANSACT = RESP.TRANSACT  """)
    result = cursor.fetchone()
    return round(result[0], 1)


@metric
def max_response_latency_in_ns(connection):
    cursor = connection.cursor()
    cursor.execute(""" SELECT max(RESP.PHASEBEGIN -  REQ.PHASEBEGIN)/1000 FROM PHASES REQ, PHASES RESP WHERE REQ.PHASENAME = 'REQ' AND RESP.PHASENAME='RESP' AND REQ.TRANSACT = RESP.TRANSACT  """)
    result = cursor.fetchone()
    return result[0]


@metric
def trans_with_max_response_latency(connection):
    cursor = connection.cursor()
    cursor.execute(""" SELECT REQ.TRANSACT, max(RESP.PHASEBEGIN -  REQ.PHASEBEGIN)/1000 FROM PHASES REQ, PHASES RESP WHERE REQ.PHASENAME = 'REQ' AND RESP.PHASENAME='RESP' AND REQ.TRANSACT = RESP.TRANSACT  """)
    result = cursor.fetchone()
    return result[0]


@metric
def memory_active(connection):
    cursor = connection.cursor()
    cursor.execute(""" SELECT sum(DataStrobeEnd - DataStrobeBegin) FROM transactions """)
    active = cursor.fetchone()
    clk, unit = getClock(connection)
    return (active[0]/clk)


@metric
def memory_total(connection):
    cursor = connection.cursor()
    cursor.execute(""" SELECT max(DataStrobeEnd) FROM Transactions """)
    total = cursor.fetchone()
    clk, unit = getClock(connection)
    return (total[0]/clk)


@metric
def memory_idle(connection):
    cursor = connection.cursor()
    cursor.execute(""" SELECT sum(p1.PhaseEnd - p2.PhaseBegin) FROM Phases p1, Phases p2 Where p1.PhaseName = "REQ" and p2.PhaseName = "RESP" and ((p1.Transact-1) = (p2.Transact)) and  (p1.PhaseEnd > p2.PhaseBegin) """)
    idle = cursor.fetchone()
    if (idle[0] is None):
        return 0
    else:
        clk, unit = getClock(connection)
        return (idle[0]/clk)


@metric
def memory_utilisation_percent_new(connection):
    total = memory_total(connection)
    active = memory_active(connection)
    idle = memory_idle(connection)
    return (active/(total-idle))*100


@metric
def memory_utilisation_in_Gibps(connection):
    # This function calculates the memory utilisation in Gibit/s considering the memory_utilisation_percent_new function result.
    maxDataRate = maximum_data_rate(connection)
    memoryPercent = memory_utilisation_percent_new(connection)
    return (memoryPercent/100)*(maxDataRate/1024)


@metric
def memory_utilisation_percent_old(connection):
    cursor = connection.cursor()
    cursor.execute(""" SELECT sum(DataStrobeEnd - DataStrobeBegin) FROM transactions """)
    active = cursor.fetchone()
    cursor = connection.cursor()
    cursor.execute(""" SELECT max(DataStrobeEnd) FROM Transactions """)
    total = cursor.fetchone()
    return (active[0]/total[0])*100


def refreshMissDecision(connection, calculatedMetrics):
    cursor = connection.cursor()
    cursor.execute("""SELECT phases.ID,PhaseBegin,PhaseEnd,TBank FROM Phases INNER JOIN  transactions on transactions.id = phases.transact WHERE PhaseName IN ('REFA')' """)
    queryMinREQ = """SELECT id,min(PhaseBegin) FROM (SELECT transactions.id, PhaseBegin FROM transactions
                      inner join ranges on ranges.id = transactions.range inner join phases on phases.transact = transactions.id
                      where tthread != 0 and tbank = :bank and PhaseName = "REQ" and ranges.begin<:begin and ranges.end>:end)"""

    queryMinRESP = """SELECT id,min(PhaseBegin) FROM (SELECT transactions.id, PhaseBegin FROM transactions
                      inner join ranges on ranges.id = transactions.range inner join phases on phases.transact = transactions.id
                      where tthread != 0 and tbank = :bank and PhaseName = "RESP" and ranges.begin<:begin and ranges.end>:end) """

    missDecisions = 0
    totalDecisios = 0

    for refresh in cursor:
        id = refresh[0]
        begin = refresh[1]
        end = refresh[2]
        bank = refresh[3]
        # print('Refresh: {0} {1} {2} {3}'.format(id,begin,end,bank))

        cursorMinREQ = connection.cursor()
        cursorMinRESP = connection.cursor()

        cursorMinREQ.execute(queryMinREQ, {"bank": bank, "begin": begin, "end": end})
        cursorMinRESP.execute(queryMinRESP, {"bank": bank, "begin": begin, "end": end})

        earliestReq = cursorMinREQ.fetchone()
        earliestResp = cursorMinRESP.fetchone()
        if (earliestReq[0] is not None):
            totalDecisios = totalDecisios + 1
            if(earliestReq[0] != earliestResp[0]):
                missDecisions = missDecisions + 1
                # print("earliest Req: {0}| earliest Res: {1}".format(earliestReq[0], earliestResp[0]))

    if (totalDecisios != 0):
        # calculatedMetrics.append(("Total Missdecisions", missDecisions))
        calculatedMetrics.append(("Relative Missdecisions", 1.0*missDecisions/totalDecisios))
    else:
        calculatedMetrics.append(("Total Missdecisions", 0))
        calculatedMetrics.append(("Relative Missdecisions", 0))


@threadMetric
def average_response_latency_in_ns(connection, thread):
    cursor = connection.cursor()
    query = """SELECT avg(PhaseBegin-timeOfGeneration)/1000 FROM transactions INNER JOIN Phases
                            ON phases.transact = transactions.ID WHERE PhaseName='RESP' AND TThread = :Thread """

    cursor.execute(query, {"Thread": thread})
    result = cursor.fetchone()
    return round(result[0], 1)


def addStallTime(times, begin, end):
    time = begin
    while time <= end:
        if(time in times):
            times[time] = times[time] + 1
        else:
            times[time] = 1
        time = time + 1


# @threadMetric
def paralellism(connection, thread):
    cursor = connection.cursor()
    stalltimes = {}
    query = """SELECT transactions.ID,MIN(phaseBegin)/:clk,MAX(phaseEnd)/:clk
              from phases inner join transactions on phases.transact=transactions.id
              where phaseName Not in ('REQ','RESP') and tthread=:Thread group by transactions.ID """

    clk, unit = getClock(connection)
    cursor.execute(query, {"Thread": thread, "clk": clk})
    for currentRow in cursor:
        addStallTime(stalltimes, currentRow[1], currentRow[2])
    para = 0
    for time in stalltimes:
        para = para + stalltimes[time]
    return round(para/len(stalltimes), 2)


@threadMetric
def thread_conclusion_in_ns(connection, thread):
    cursor = connection.cursor()
    query = """ SELECT max(PhaseEnd)/1000 FROM Phases INNER JOIN Transactions on Phases.transact=Transactions.id WHERE TThread = :Thread """
    cursor.execute(query, {"Thread": thread})
    result = cursor.fetchone()
    return result[0]


@metric
def REQ_count(connection):
    return get_phase_occurrences(connection, 'REQ')


@metric
def RESP_count(connection):
    return get_phase_occurrences(connection, 'RESP')


@metric
def PREB_count(connection):
    return get_phase_occurrences(connection, 'PREB')


@metric
def PRE_count(connection):
    return get_phase_occurrences(connection, 'PRE')


@metric
def ACTB_count(connection):
    return get_phase_occurrences(connection, 'ACTB')


@metric
def ACT_count(connection):
    return get_phase_occurrences(connection, 'ACT')


@metric
def PRE_ALL_count(connection):
    return get_phase_occurrences(connection, 'PRE_ALL')


@metric
def REFA_count(connection):
    return get_phase_occurrences(connection, 'REFA')


@metric
def REFB_count(connection):
    return get_phase_occurrences(connection, 'REFB')


@metric
def RD_count(connection):
    return get_phase_occurrences(connection, 'RD')


@metric
def RDA_count(connection):
    return get_phase_occurrences(connection, 'RDA')


@metric
def WR_count(connection):
    return get_phase_occurrences(connection, 'WR')


@metric
def WRA_count(connection):
    return get_phase_occurrences(connection, 'WRA')


@metric
def PDNA_count(connection):
    return get_phase_occurrences(connection, 'PDNA')


@metric
def PDNB_count(connection):
    return get_phase_occurrences(connection, 'PDNAB')


@metric
def PDNP_count(connection):
    return get_phase_occurrences(connection, 'PDNP')


@metric
def PDNPB_count(connection):
    return get_phase_occurrences(connection, 'PDNPB')


@metric
def SREF_count(connection):
    return get_phase_occurrences(connection, 'SREF')


@metric
def SREFB_count(connection):
    return get_phase_occurrences(connection, 'SREFB')


@metric
def number_of_accesses(connection):
    return REQ_count(connection)


@metric
def accesses_per_activate(connection):
    return round(float(REQ_count(connection)) / ACT_count(connection), 1)


@metric
def bank_overlap_ratio(connection):
    # Calculates how many banks are open in parallel
    clk, unit = getClock(connection)

    cursor = connection.cursor()
    cursor.execute("select TraceEnd from GeneralInfo")
    traceEndTMP = cursor.fetchone()
    traceEnd = int(traceEndTMP[0]/clk)
    trace = []

    cursor.execute("""
        select p1.PhaseBegin, p1.PhaseName from Phases p1
        where
        (p1.PhaseName = "ACT" or p1.PhaseName = "PRE_ALL" or p1.PhaseName = "PRE" or p1.PhaseName = "RDA" or p1.PhaseName = "WRA" or p1.PhaseName = "PREB" or p1.PhaseName = "ACTB")
        order by PhaseBegin
        """)
    prevPhase = "PRE_ALL"
    prevTime = 0

    for c in cursor:
        trace.append([(int(c[0]/clk)), c[1]])

    # Insert a pseudo precharge all to mark the end of the trace
    trace.append([traceEnd, "PRE_ALL"])

    bankCounter = 0
    bankTime = []

    for i in range(0, getNumberOfBanks(connection)+1):
        bankTime.append(0)

    currentTime = 0
    validBankRange = range(0, getNumberOfBanks(connection) + 1)

    # RGR uses ACTB and PREB for refresh
    actbCnt = 0
    for t in trace:

        interval = t[0] - currentTime
        bankTime[bankCounter] += interval
        currentTime = t[0]

        if(t[1] == "ACT"):
            bankCounter += 1
            if not (bankCounter in validBankRange):
                print("Unexpected ACT. bankCounter was {0} valid range is python range {1}".format(bankCounter, validBankRange))
        elif t[1] == "ACTB":
            actbCnt += 1
        elif(t[1] == "PRE_ALL"):
            bankCounter = 0
        elif(t[1] == "PRE"):
            if (bankCounter > 0):
                bankCounter -= 1
            else:
                print("Unexpected PRE. bankCounter was {0} valid range is python range {1}".format(bankCounter, validBankRange))
        elif(t[1] == "PREB"):
            # RGR first PREB closes the row ACT follows, then PREB again.
            if actbCnt > 0:
                # Ignore the second PREB
                actbCnt -= 1
            else:
                # Treat the first PREB which closes the row
                if bankCounter > 0:
                    bankCounter -= 1
                else:
                    print("Unexpected PREB. bankCounter was {0} valid range is python range {1}".format(bankCounter, validBankRange))
        elif(t[1] == "WRA"):
            if (bankCounter > 0):
                bankCounter -= 1
            else:
                print("Unexpected WRA. bankCounter was {0} valid range is python range {1}".format(bankCounter, validBankRange))
        elif(t[1] == "RDA"):
            if (bankCounter > 0):
                bankCounter -= 1
            else:
                print("Unexpected RDA. bankCounter was {0} valid range is python range {1}".format(bankCounter, validBankRange))
        else:
            print("ERROR")
            return 0

    for i in range(0, getNumberOfBanks(connection)+1):
        bankTime[i] = round(bankTime[i]/traceEnd * 100, 2)

    return ",".join(format(x, "6.2f") for x in bankTime)


# @metric
# def number_of_precharges(connection):
#     cursor = connection.cursor()
#     cursor.execute("SELECT COUNT(*) FROM Phases WHERE PhaseName IN ('PRE','PRE_ALL','RDA','WRA')")
#     result = cursor.fetchone()
#     return result[0]


def time_in_PDNA_in_ns(connection):
    return get_total_time_in_phase(connection, "PDNA")


def time_in_PDNA_percent(connection):
    return (time_in_PDNA_in_ns(connection) * 1.0 / trace_length_in_ns(connection)) * 100


def time_in_PDNP_in_ns(connection):
    return get_total_time_in_phase(connection, "PDNP")


def time_in_PDNP_percent(connection):
    return (time_in_PDNP_in_ns(connection) * 1.0 / trace_length_in_ns(connection)) * 100


def time_in_SREF_in_ns(connection):
    return get_total_time_in_phase(connection, "SREF")


def time_in_SREF_percent(connection):
    return (time_in_SREF_in_ns(connection) * 1.0 / trace_length_in_ns(connection)) * 100


def time_in_PDNAB_in_ns(connection):
    return get_total_time_in_phase(connection, "PDNAB")


def time_in_PDNAB_percent(connection):
    totalTimeAllBanks = trace_length_in_ns(connection) * getNumberOfBanks(connection)
    return (time_in_PDNAB_in_ns(connection) * 1.0 / totalTimeAllBanks) * 100


def time_in_PDNPB_in_ns(connection):
    return get_total_time_in_phase(connection, "PDNPB")


def time_in_PDNPB_percent(connection):
    totalTimeAllBanks = trace_length_in_ns(connection) * getNumberOfBanks(connection)
    return (time_in_PDNPB_in_ns(connection) * 1.0 / totalTimeAllBanks) * 100


def time_in_SREFB_in_ns(connection):
    return get_total_time_in_phase(connection, "SREFB")


def time_in_SREFB_percent(connection):
    totalTimeAllBanks = trace_length_in_ns(connection) * getNumberOfBanks(connection)
    return (time_in_SREFB_in_ns(connection) * 1.0 / totalTimeAllBanks) * 100


@metric
def time_in_power_down_states_in_ns(connection):
    mcconfig = MCConfig(connection)
    bankwiseLogic = mcconfig.getValue("BankwiseLogic")
    if bankwiseLogic == "0":
        totalTimeInPDNA = time_in_PDNA_in_ns(connection)
        totalTimeInPDNP = time_in_PDNP_in_ns(connection)
        totalTimeInSREF = time_in_SREF_in_ns(connection)
        totalTimePdnStates = totalTimeInPDNA + totalTimeInPDNP + totalTimeInSREF
    else:
        totalTimeInPDNAB = time_in_PDNAB_in_ns(connection)
        totalTimeInPDNPB = time_in_PDNPB_in_ns(connection)
        totalTimeInSREFB = time_in_SREFB_in_ns(connection)
        totalTimePdnStates = totalTimeInPDNAB + totalTimeInPDNPB + totalTimeInSREFB

    return totalTimePdnStates


@metric
def time_in_power_down_states_percent(connection):
    mcconfig = MCConfig(connection)
    bankwiseLogic = mcconfig.getValue("BankwiseLogic")
    if bankwiseLogic == "0":
        totalTimeAllBanks = trace_length_in_ns(connection)
    else:
        totalTimeAllBanks = trace_length_in_ns(connection) * getNumberOfBanks(connection)
    return (time_in_power_down_states_in_ns(connection) * 1.0 / totalTimeAllBanks) * 100


def passRatio(connection):

    numberOfPassWins = {}
    numberOfPassLosses = {}

    for thread in getThreads(connection):
        numberOfPassWins[thread] = 0
        numberOfPassLosses[thread] = 0

    for bankNumber in range(getNumberOfBanks(connection)):
        cursor = connection.cursor()
        query = """SELECT begin,end,transactions.ID,TThread,
                      TBank from transactions inner join ranges on transactions.range = ranges.id WHERE TBank = :Bank AND TThread != 0"""
        cursor.execute(query, {"Bank": bankNumber})

        for passedRequest in cursor:
            passedBegin = passedRequest[0]
            passedEnd = passedRequest[1]
            passedId = passedRequest[2]
            passedThread = passedRequest[3]

            cursor2 = connection.cursor()
            query2 = """SELECT begin,end,transactions.ID,TThread,
                             TBank from transactions inner join ranges on transactions.range = ranges.id
                             WHERE TBank = :Bank AND :Begin<begin AND end<:End AND TThread != :Thread AND TThread != 0"""
            cursor2.execute(query2, {"Bank": bankNumber, "Thread": passedThread, "Begin": passedBegin, "End": passedEnd})

            for passingRequest in cursor2:
                numberOfPassLosses[passedThread] += 1
                numberOfPassWins[passingRequest[3]] += 1
                # print("""Transaction {0} (thread:{1} begin:{2} end:{3}) was passed by Transaction {4} (thread:{5} begin:{6} end:{7}) on bank {8}""".format(passedId,passedThread, passedBegin,passedEnd,
                #              passingRequest[2], passingRequest[3], passingRequest[0], passingRequest[1], bankNumber))

    result = []
    for thread in getThreads(connection):
        totalPassedInvolved = numberOfPassWins[thread]+numberOfPassLosses[thread]
        if(totalPassedInvolved > 0):
            passRatio = numberOfPassWins[thread]*1.0/(numberOfPassWins[thread]+numberOfPassLosses[thread])
        else:
            passRatio = 0.5
        # print("Thread {0} passed other threads {1} times and was passed {2} times. Pass ratio is {3}".format(thread, numberOfPassWins[thread], numberOfPassLosses[thread], passRatio))
        result.append(("Thread {0} pass ratio".format(thread), passRatio))
    return result


def getMetrics(pathToTrace):
    selectedMetrics = []
    connection = sqlite3.connect(pathToTrace)

    mcconfig = MCConfig(connection)
    bankwiseLogic = mcconfig.getValue("BankwiseLogic")

    if bankwiseLogic == "0":
        pdnMetrics = [time_in_PDNA_in_ns, time_in_PDNA_percent, time_in_PDNP_in_ns, time_in_PDNP_percent, time_in_SREF_in_ns, time_in_SREF_percent]
    else:
        pdnMetrics = [time_in_PDNAB_in_ns, time_in_PDNAB_percent, time_in_PDNPB_in_ns, time_in_PDNPB_percent, time_in_SREFB_in_ns, time_in_SREFB_percent]

    for metric in pdnMetrics:
        if metric not in metrics:
            metrics.append(metric)

    if (len(getThreads(connection)) >= 1):
        for metric in metrics:
            res = metric.__name__.replace("_", " ")
            selectedMetrics.append(res)

    if (len(getThreads(connection)) > 1):
        for thread in getThreads(connection):
            for threadMetric in threadMetrics:
                res = "Thread {0}: {1}".format(thread, threadMetric.__name__.replace("_", " "))
                selectedMetrics.append(res)
        res = "pass ratio"
        selectedMetrics.append(res)

    connection.close()
    return selectedMetrics


def calculateMetrics(pathToTrace, selectedMetrics=[]):

    calculatedMetrics = []
    connection = sqlite3.connect(pathToTrace)

    mcconfig = MCConfig(connection)
    bankwiseLogic = mcconfig.getValue("BankwiseLogic")

    if bankwiseLogic == "0":
        pdnMetrics = [time_in_PDNA_in_ns, time_in_PDNA_percent,
                      time_in_PDNP_in_ns, time_in_PDNP_percent,
                      time_in_SREF_in_ns, time_in_SREF_percent]
    else:
        pdnMetrics = [time_in_PDNAB_in_ns, time_in_PDNAB_percent,
                      time_in_PDNPB_in_ns, time_in_PDNPB_percent,
                      time_in_SREFB_in_ns, time_in_SREFB_percent]

    for m in pdnMetrics:
        if m not in metrics:
            metrics.append(m)

    print("================================")
    print("Calculating metrics for {0}".format(pathToTrace))

    print("Number of threads is {0}".format(len(getThreads(connection))))

    if not selectedMetrics:
        selectedMetrics = [0] * (len(metrics) + len(getThreads(connection))*len(threadMetrics) + 1)
        for i in range(len(selectedMetrics)):
            selectedMetrics[i] = True

    if (len(getThreads(connection)) >= 1):
        for metric in metrics:
            if selectedMetrics[metrics.index(metric)]:
                mres = metric(connection)
                mname = metric.__name__.replace("_", " ")
                res = (mname, mres)

                if (metric.__name__ == "bank_overlap_ratio"):
                    values = mres.split(",")
                    nbanks = 0
                    for v in values:
                        name = mname + " (" + str(nbanks) + " banks active)"
                        nbanks = nbanks + 1
                        r = (name, float(v))
                        calculatedMetrics.append(r)
                else:
                    calculatedMetrics.append(res)

                print("{0}: {1}".format(res[0], res[1]))

    if (len(getThreads(connection)) > 1):
        for thread in getThreads(connection):
            for metric in threadMetrics:
                if(selectedMetrics[len(metrics) + len(threadMetrics)*(thread-1) + threadMetrics.index(metric)]):
                    mres = metric(connection, thread)
                    mname = "Thread {0} - {1}".format(thread, metric.__name__.replace("_", " "))
                    res = (mname, mres)
                    calculatedMetrics.append(res)
                print("{0}: {1}".format(res[0], res[1]))

        if (selectedMetrics[len(selectedMetrics) - 1]):
            calculatedMetrics.extend(passRatio(connection))
        # refreshMissDecision(connection, calculatedMetrics)

    if (len(getThreads(connection)) == 0):
        res = ("No accesses were performed for this channel, number of metrics generated", 0.0)
        calculatedMetrics.append(res)

    # refreshMissDecision(connection, calculatedMetrics)
    connection.close()
    return calculatedMetrics

if __name__ == "__main__":
    path = sys.argv[1]
    calculateMetrics(path)
