#!/usr/bin/env python3

# Copyright © Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier:  MIT

import sys

if sys.version_info[0] < 3:
    raise Exception("Must be using Python 3")

import numpy as np
from io import BytesIO
import matplotlib.pyplot as plt
from copy import deepcopy
import json

COUNTERS_MAX_CAPTURES = 1 << 12


class Readable:
    def __init__(self, jsonstring):
        self.jsonstr = json.dumps(jsonstring)
        self.seek = 0

    def read(self, length=0):
        if length <= 0:
            return self.jsonstr
        else:
            if self.seek >= len(self):
                self.seek = 0
                return None
            response = self.jsonstr[self.seek : self.seek + length]
            self.seek += length
            return bytes(response, "utf-8")

    def __len__(self):
        return len(self.jsonstr)


class FileBytesIO:
    def __init__(self, iobytes):
        self.iobytes = deepcopy(iobytes)
        self.seek = 0

    def __len__(self):
        return self.iobytes.getbuffer().nbytes

    def read(self, length=0):
        if length <= 0:
            return bytes(self.iobytes.getbuffer())
        else:
            if self.seek >= self.iobytes.getbuffer().nbytes:
                self.seek = 0
                return None
            response = self.iobytes.getbuffer()[self.seek : self.seek + length]
            self.seek += length
            return bytes(response)


def get_delta_time(events):
    try:
        CUS = [[e.time for e in events if e.cu == k and e.bank == 0] for k in range(16)]
        CUS = [np.asarray(c).astype(np.int64) for c in CUS if len(c) > 2]
        return np.min([np.min(abs(c[1:] - c[:-1])) for c in CUS])
    except:
        return 1


def draw_wave_metrics(selections, normalize, TIMELINES, EVENTS, EVENT_NAMES):
    plt.figure(figsize=(15, 4))

    delta_step = 8
    quad_delta_time = max(
        delta_step, int(0.5 + np.min([get_delta_time(events) for events in EVENTS]))
    )
    maxtime = (
        np.max([np.max([e.time for e in events]) for events in EVENTS]) / quad_delta_time
        + 1
    )

    if maxtime * delta_step >= COUNTERS_MAX_CAPTURES:
        delta_step = 1
    while maxtime >= COUNTERS_MAX_CAPTURES:
        quad_delta_time *= 2
        maxtime /= 2

    maxtime = int(min(maxtime * delta_step, COUNTERS_MAX_CAPTURES))
    event_timeline = np.zeros((16, maxtime), dtype=np.int32)
    print("Delta:", quad_delta_time)
    print("Max_cycles:", maxtime * quad_delta_time * 4 // delta_step)

    cycles = 4 * quad_delta_time // delta_step * np.arange(maxtime)
    kernel = len(EVENTS) * quad_delta_time

    for events in EVENTS:
        for e in range(len(events) - 1):
            bk = events[e].bank * 4
            start = events[e].time // (quad_delta_time // delta_step)
            end = start + delta_step
            event_timeline[bk : bk + 4, start:end] += np.asarray(
                events[e].toTuple()[1:5]
            )[:, None]
        start = events[-1].time
        event_timeline[bk : bk + 4, start : start + delta_step] += np.asarray(
            events[-1].toTuple()[1:5]
        )[:, None]

    event_timeline = [
        np.convolve(e, [kernel for k in range(3)])[1:-1] for e in event_timeline
    ]
    # event_timeline = [e/kernel for e in event_timeline]

    if normalize:
        event_timeline = [100 * e / max(e.max(), 1e-5) for e in event_timeline]

    colors = [
        "blue",
        "green",
        "gray",
        "red",
        "orange",
        "cyan",
        "black",
        "darkviolet",
        "yellow",
        "darkred",
        "pink",
        "lime",
        "gold",
        "tan",
        "aqua",
        "olive",
    ]
    [
        plt.plot(cycles, e, "-", label=n, color=c)
        for e, n, c, sel in zip(event_timeline, EVENT_NAMES, colors, selections)
        if sel
    ]

    plt.legend()
    if normalize:
        plt.ylabel("As % of maximum")
    else:
        plt.ylabel("Value")
    plt.xlabel("Cycle")
    plt.subplots_adjust(left=0.04, right=1, top=1, bottom=0.1)

    figure_bytes = BytesIO()
    plt.savefig(figure_bytes, dpi=150)
    return EVENT_NAMES, FileBytesIO(figure_bytes)


def draw_wave_states(selections, normalize, TIMELINES):
    plot_indices = [1, 2, 3, 4]
    STATES = [["Empty", "Idle", "Exec", "Wait", "Stall"][k] for k in plot_indices]
    colors = [["gray", "orange", "green", "red", "blue"][k] for k in plot_indices]

    plt.figure(figsize=(15, 4))


    maxtime = max([np.max((TIMELINES[k]!=0)*np.arange(0,TIMELINES[k].size)) for k in plot_indices])
    maxtime = max(maxtime, 1)
    timelines = [deepcopy(TIMELINES[k][:maxtime]) for k in plot_indices]
    timelines = [np.pad(t, [0, maxtime - t.size]) for t in timelines]

    if normalize:
        timelines = np.array(timelines) / np.maximum(np.sum(timelines, 0) * 1e-2, 1e-7)

    trim = max(maxtime // 5000, 1)
    cycles = np.arange(0, timelines[0].size // trim, 1) * trim
    timelines = [
        time[: trim * (time.size // trim)].reshape((-1, trim)).mean(-1)
        if len(time) > 0
        else cycles * 0
        for time in timelines
    ]
    kernsize = 15
    kernel = np.asarray([
        np.exp(-abs(10 * k / kernsize)) for k in range(-kernsize // 2, kernsize // 2 + 1)
    ])
    kernel /= np.sum(kernel)

    timelines = [
        np.convolve(time, kernel)[kernsize // 2 : -kernsize // 2]
        for time in timelines if len(time) > 0
    ]
    maxtime *= 16
    cycles *= 16
    [
        plt.plot(cycles, t, label="State " + s, linewidth=1.1, color=c)
        for t, s, c, sel in zip(timelines, STATES, colors, selections)
        if sel
    ]

    plt.legend()
    if normalize:
        plt.ylabel("Waves state %")
    else:
        plt.ylabel("Waves state total")
    plt.xlabel("Cycle")
    plt.ylim(-1)
    plt.xlim(-maxtime // 200, maxtime + maxtime // 200 + 1)
    plt.subplots_adjust(left=0.04, right=1, top=1, bottom=0.1)
    figure_bytes = BytesIO()
    plt.savefig(figure_bytes, dpi=150)
    return STATES, FileBytesIO(figure_bytes)


def draw_occupancy_per_dispatch(selections, normalize, OCCUPANCY, dispatchnames):
    plt.figure(figsize=(15, 4))
    maxtime = 1
    delta = 1

    for k in range(len(OCCUPANCY)):
        if len(OCCUPANCY[k]) <= 1:
            continue
        for ev in OCCUPANCY[k]:
            maxtime = max(maxtime, ev[0])

    NUM_DOTS = 1600 # number of points taken for graphing
    delta = max(1, maxtime // NUM_DOTS) # Spacing between data points. Waves will be averaged over this interval.
    # Holds occupancy data
    chart = np.zeros((len(dispatchnames), maxtime // delta + 2), dtype=np.float32)

    for occ in OCCUPANCY:
        if len(occ) <= 1:
            continue
        # Number of waves multiplied by number of events
        small_chart = np.zeros_like(chart)
        # Holds number of events in that time period, for averaging.
        norm_fact = np.zeros_like(chart)
        norm_fact += 1E-5

        # Holds last known state per dispatch
        current_time = [0 for k in range(len(dispatchnames))]
        # Holds occupancy per Dispatch
        total_value = [0 for k in range(len(dispatchnames))]

        for time, en, kid in occ:
            b = current_time[kid]
            e = max(b + 1, time // delta)
            small_chart[kid][b:e] += total_value[kid]
            norm_fact[kid][b:e] += 1

            # Enable = 1 means a new wave started, enable = 0 means a wave has ended on that kernel ID.
            total_value[kid] += 2*en - 1
            current_time[kid] = time // delta
        for small, norm, time, value in zip(small_chart, norm_fact, current_time, total_value):
            small[time] += value
            norm[time] += value

        chart += small_chart/norm_fact # small_chart / norm_fact is the mean number of waves a tthat time point

    for (id, name), occ in zip(dispatchnames.items(), chart):
        plt.plot(np.arange(occ.size) * delta * 8, occ, label=str(id)+'#'+name, linewidth=1.1)

    plt.legend()
    if normalize:
        plt.ylabel("Occupancy %")
    else:
        plt.ylabel("Occupancy total")
    plt.xlabel("Cycle")
    plt.ylim(-1)
    plt.xlim(-maxtime // 200, maxtime + maxtime // 200 + delta + 1)
    plt.subplots_adjust(left=0.04, right=1, top=1, bottom=0.1)
    figure_bytes = BytesIO()
    plt.savefig(figure_bytes, dpi=150)
    return dispatchnames, FileBytesIO(figure_bytes)


def draw_occupancy(selections, normalize, OCCUPANCY, shadernames, numdispatchid):
    plt.figure(figsize=(15, 4))
    names = []

    g_maxtime = 1
    g_delta = 1
    for name, occ in zip(shadernames, OCCUPANCY):
        if len(occ) <= 1:
            continue

        occ_values = [0]
        occ_times = [0]

        for time, en, _ in occ:
            occ_times.append(time)
            occ_values.append(occ_values[-1] + 2*en - 1) # If enable = 1, increment. Else, decrement occupancy.

        try:
            names.append('SE'+name.split('_se')[1].split('.att')[0])
        except:
            names.append(name)

        NUM_DOTS = 1500 # Number of points taken for graphing
        maxtime = occ_times[-1]+1
        delta = max(1, maxtime // NUM_DOTS)
        g_maxtime = max(g_maxtime, maxtime)
        g_delta = max(g_delta, delta)
        chart = np.zeros((maxtime // delta + 1), dtype=np.float32)
        norm_fact = np.zeros_like(chart)
        norm_fact += 1E-6

        for i in range(len(occ_times)-1):
            b = occ_times[i] // delta
            e = max(b + 1, occ_times[i + 1] // delta)
            chart[b:e] += occ_values[i]
            norm_fact[b:e] += 1

        chart /= norm_fact
        if normalize:
            chart /= max(chart.max(), 1e-6)

        plt.plot(np.arange(chart.size) * delta, chart, label=names[-1], linewidth=1.1)

    plt.legend()
    if normalize:
        plt.ylabel("Occupancy %")
    else:
        plt.ylabel("Occupancy total")
    plt.xlabel("Cycle")
    plt.ylim(-1)
    plt.xlim(-g_maxtime // 200, g_maxtime + g_maxtime // 200 + g_delta + 1)
    plt.subplots_adjust(left=0.04, right=1, top=1, bottom=0.1)
    figure_bytes = BytesIO()
    plt.savefig(figure_bytes, dpi=150)
    return names, FileBytesIO(figure_bytes)


def getocc(u):
    # Parser struct occupancy_info_t
    # Bits 23:63 Time= Time divided by 8
    # Bit 18 = Enable (Wave start if 1, Wave end if 0)
    # Bits 0:11 is the kernel ID running on that wave
    return 8*int(u>>23), (u>>18) & 1, u&0xFFF


def GeneratePIC(drawinfo, selections=[True for k in range(16)], normalize=False):
    EVENTS = drawinfo["EVENTS"]

    response = {}
    figures = {}

    OCCUPANCY = drawinfo["OCCUPANCY"]
    # Transforms returned data into a array of events with each event being a tuple (time, enable, kernel ID)
    OCCUPANCY = [[getocc(u) for u in OCCUPANCY[k]] for k in range(len(OCCUPANCY))]

    states, figure = draw_occupancy(selections, normalize, OCCUPANCY, drawinfo["ShaderNames"], len(drawinfo["DispatchNames"]))
    response["occupancy.png"] = states
    figures["occupancy.png"] = figure

    states, figure = draw_occupancy_per_dispatch(selections, normalize, OCCUPANCY, drawinfo["DispatchNames"])
    response["dispatches.png"] = states
    figures["dispatches.png"] = figure

    states, figure = draw_wave_states(selections, normalize, drawinfo["TIMELINES"])
    response["timeline.png"] = states
    figures["timeline.png"] = figure

    if len(EVENTS) > 0 and np.sum([len(e) for e in EVENTS]) > 32:
        EVENT_NAMES, figure = draw_wave_metrics(
            selections, normalize, drawinfo["TIMELINES"], EVENTS, drawinfo["EVENT_NAMES"]
        )
        response["counters.png"] = EVENT_NAMES
        figures["counters.png"] = figure

    return Readable(response), figures
