#
# Microlog. Copyright (c) 2023 laffra, dcharbon. All rights reserved.
#

from __future__ import annotations

from collections import defaultdict
import js # type: ignore
import itertools
import math
import pyodide # type: ignore
from typing import List

from microlog.models import Call
from dashboard import profiler

from dashboard.dialog import dialog

from dashboard import config
from dashboard.views import View
from dashboard.views import sanitize
from dashboard.views import status

from dashboard import canvas
from dashboard import colors


class CallView(View):
    model = Call
    threadIndex = defaultdict(lambda: len(CallView.threadIndex))
    showThreads = set()
    instances = []
    minWidth = 3
    selected = None

    @profiler.profile("CallView.__init__")
    def __init__(self, canvas: canvas.Canvas, model: dict):
        View.__init__(self, canvas, model)
        self.h = config.LINE_HEIGHT
        self.y = self.depth * config.LINE_HEIGHT + 200 * self.getThreadIndex(self.canvas, self.threadId)
        self.color = colors.getColor(self.callSite.name)
        self.index = len(CallView.instances)
        CallView.instances.append(self)

    def getLabel(self):
        w = self.canvas.toScreenDimension(self.w)
        if w > 300:
            return f"{self.getFullName()} (at {self.when:0.2f} duration: {self.duration:0.2f}s)"
        elif w > 200:
            return f"{self.getShortName()} (at {self.when:0.2f} duration: {self.duration:0.2f}s)"
        else:
            return self.getShortName()

    @classmethod
    def reset(cls):
        CallView.instances.clear()
        CallView.selected = None
        cls.threadIndex = defaultdict(lambda: len(CallView.threadIndex))
        js.jQuery(".thread-selector").remove()

    @classmethod
    @profiler.profile("CallView.getThreadIndex")
    def getThreadIndex(cls, canvas, threadId):
        if not threadId in cls.threadIndex:
            cls.showThreads.add(threadId)
            def redraw(event):
                if threadId in cls.showThreads:
                    cls.showThreads.remove(threadId)
                else:
                    cls.showThreads.add(threadId)
                canvas.redraw()
            js.jQuery(".flamegraph-container").append(
                (js.jQuery("<input>")
                    .addClass("thread-selector")
                    .prop("type", "checkbox")
                    .prop("checked", "checked")
                    .attr("id", f"toggle-{threadId}")
                    .attr("threadId", threadId)
                    .css("top", canvas.offsetY + 227 + 200 * len(cls.threadIndex))
                    .on("change", pyodide.ffi.create_proxy(redraw))),
                js.jQuery("#timelineCanvas"),
            )
        return cls.threadIndex[threadId]

    @classmethod
    def positionThreads(self, canvas):
        def setTop(index, element):
            js.jQuery(element).css("top", canvas.offsetY + 227 + 200 * index)
        js.jQuery(".thread-selector").each(pyodide.ffi.create_proxy(setTop))

    @classmethod
    @profiler.profile("CallView.drawAll")
    def drawAll(cls, canvas: canvas.Canvas, calls):
        canvas.clear("#222")
        canvas.fillRects(
            (call.x, call.y, call.w, call.h, call.color)
            for call in calls
            if canvas.toScreenDimension(call.w) > cls.minWidth and call.threadId in cls.showThreads
        )
        dx = canvas.fromScreenDimension(4)
        canvas.texts(
            [
                (
                    call.x + dx,
                    call.y + call.h - 8,
                    call.getLabel(),
                    "#111111",
                    call.w - 2 * dx
                )
                for call in calls
                if canvas.toScreenDimension(call.w) > cls.minWidth and call.threadId in cls.showThreads
            ], config.FONT_REGULAR
        )
        if len(js.jQuery(".thread-selector")) < 2:
            js.jQuery(".thread-selector").css("display", "none")
        canvas.lines(
            itertools.chain([
                ( call.x, call.y, call.x, call.y + call.h )
                for call in calls
                if canvas.toScreenDimension(call.w) > cls.minWidth and call.threadId in cls.showThreads
            ]),
            1,
            "gray"
        )
        canvas.lines(
            itertools.chain([
                ( call.x, call.y + call.h, call.x + call.w, call.y + call.h )
                for call in calls
                if canvas.toScreenDimension(call.w) > cls.minWidth and call.threadId in cls.showThreads
            ]),
            1,
            "gray"
        )
        if cls.selected:
            cls.selected.draw("red", "white")

    def inside(self, x, y):
        return self.threadId in self.showThreads and self.canvas.toScreenDimension(self.w) > self.minWidth and View.inside(self, x, y)

    def draw(self, fill, color):
        if not self.threadId in self.showThreads:
            return
        w = self.canvas.toScreenDimension(self.w)
        if w > 0:
            self.canvas.fillRect(self.x, self.y, self.w, self.h - 1, fill)
            self.canvas.line(self.x, self.y, self.x + self.w, self.y, 1, "#DDD")
            self.canvas.line(self.x, self.y, self.x, self.y + self.h, 1, "#AAA")
        if w > self.minWidth:
            dx = self.canvas.fromScreenDimension(4)
            self.canvas.text(self.x + dx, self.y + 2, self.getLabel(), color, self.w - 2 * dx)

    def click(self, x, y):
        if not self.canvas.isDragging():
            self.showPopup(x, y)

    def showPopup(self, x, y):
        if self.canvas.isDragging():
            return
        if dialog.showing and CallView.selected is self:
            dialog.hide()
            return
        CallView.selected = self
        self.canvas.redraw()
        similar = [call for call in self.instances if self.isSimilar(call)]
        average = sum(call.duration for call in similar) / len(similar) if similar else 0
        anomalies = [call for call in similar if call.duration - average > 0.1 and call.duration / average > 1.3]
        cpu = self.getCpu()
        detailsId = f"call-details-{id(self)}"
        name = sanitize(self.callSite.name).replace("..",".")
        link = f"<a href=vscode://file/{self.callSite.filename}:{self.callSite.lineno}:1>{name}</a>"
        dialog.show(self.canvas, x, y, f"""
            <b>{link}</b><br>
            This call happened at: {self.when:.3f}s<br>
            It lasted for: {self.duration:.3f}s<br>
            Average duration: {average:.3f}s<br>
            CPU usage during this call: {cpu:.1f}s {"😡" if cpu < 80 else ""}<br>
            Called by: {sanitize(self.callerSite.name).replace("..", ".")}<br>
            <div id="{detailsId}"><br><span style="color:gray">loading details...</span></div>
        """)
        if len(similar) > 1000:
            js.setTimeout(pyodide.ffi.create_proxy(lambda: self.addSimilarCalls(f"#{detailsId}", cpu, similar, anomalies)), 1)
        else:
            self.addSimilarCalls(f"#{detailsId}", cpu, similar, anomalies)
        js.jQuery(".call-index").click(pyodide.ffi.create_proxy(lambda event: self.highlightCall(js.jQuery(event.target))))
        self.draw("red", "white")

    def highlightCall(self, link):
        CallView.selected = CallView.instances[int(link.attr("index"))]
        self.canvas.redraw()

    def mouseenter(self, x, y):
        View.mouseenter(self, x, y)
        left = self.canvas.toScreenX(self.x)
        top = self.canvas.toScreenY(self.y) + config.FLAME_OFFSET_Y + config.TIMELINE_HEIGHT + 4
        w = self.canvas.toScreenDimension(self.w)
        h = config.LINE_HEIGHT
        if self.threadId in self.showThreads and self.canvas.toScreenDimension(self.w) > self.minWidth:
            js.jQuery(".call-highlight").appendTo(self.canvas.canvas.parent())
            js.jQuery("#call-highlight-top").css("left", left).css("top", top).css("width", w).css("height", 0)
            js.jQuery("#call-highlight-bottom").css("left", left).css("top", top + h).css("width", w).css("height", 0)
            js.jQuery("#call-highlight-left").css("left", left).css("top", top).css("width", 0).css("height", h)
            js.jQuery("#call-highlight-right").css("left", left + w).css("top", top).css("width", 0).css("height", h)

    def mouseleave(self, x, y):
        View.mouseleave(self, x, y)
        
    def getCpu(self):
        stats = [
            view
            for view in status.StatusView.instances
            if view.when >= self.when and view.when <= self.when + self.duration
        ]
        return sum(stat.cpu for stat in stats) / len(stats) if stats else 0

    def isAnomaly(self, call, anomalies):
        for anomaly in anomalies:
            if call is anomaly:
                return True

    def getAllCalls(self, similar, anomalies):
        maxDuration = max(call.duration for call in similar)
        def color(call: CallView):
            return "red" if self.isAnomaly(call, anomalies) else "green"
        return "".join([
            f"""<tr>
                <td class="td-number"><a class="call-index" index={call.index} href=#>{call.when:.3f}s</a></td>
                <td class="td-number">{call.duration:.3f}s</td>
                <td><div style="background: {color(call)}; height: 12px; width:{call.duration * 150 / maxDuration}px"></div></td>
            </tr>"""
            for call in sorted(similar, key=lambda call: -call.duration)
        ])

    def getSimilarCallHTML(self, similar, anomalies):
        return f"""<br>There are {len(similar)} calls in this run:
            <div style="height: 300px; overflow-y: scroll">
                <table>
                    <hr>
                        <td style="text-align: right"><b>When</b></td>
                        <td style="text-align: right"><b>&nbsp;&nbsp;Time</b></td>
                </hr>
                {self.getAllCalls(similar, anomalies)}
                </table>
            </div>
        """
            
    def getAnomaliesHTML(self, cpu, anomalies):
        return f"""
            <br>
            <img src="data:image/webp;base64, ">
            <br>
            Microlog detected {len(anomalies)} anomalies.<br>
            The current call {"appears to be an anomaly" if self.isAnomaly(self, anomalies) else "looks average"}.<br>
        """

    def addSimilarCalls(self, detailsId: str, cpu: float, similar: List[Call], anomalies: List[Call]):
        parts = []
        if math.ceil(cpu) > 0 and cpu < 80:
            parts.append("<span style='color:red'><b>CPU usage is low during this call.</b></span><br>")
        if len(anomalies) > 1:
            parts.append(self.getAnomaliesHTML(cpu, anomalies))
        if len(similar) > 1:
            parts.append(self.getSimilarCallHTML(similar, anomalies))
        js.jQuery(detailsId).html("".join(parts))

    def __eq__(self, other):
        return isinstance(other, CallView) and self.callSite == other.callSite and self.callerSite == other.callerSite