#include <Utilities/Perf.hpp>
#include <Log/Log.hpp>
#include <Settings/Settings.hpp>

static constexpr std::chrono::duration markerPeriod = 5ms;
static constexpr std::chrono::duration graphTimeResolution = 250us;
static constexpr std::chrono::duration chartTimeResolution = 10us;
static constexpr int chartBarHeight = 30;

static void drawEvent(
    GraphicsContext &context,
    const PerfTimeline::Event &event,
    int x, int bottomY,
    glm::vec4 color
)
{
    context.color = color;

    for (const PerfTimeline::Interval &interval : event.intervals())
    {
        context.drawRect(x, bottomY - interval.end / graphTimeResolution, 1, (interval.end - interval.start) / graphTimeResolution);
    }
}

static void drawEventThreadTime(
    GraphicsContext &context,
    const PerfTimeline::Event &event,
    int x, int bottomY,
    glm::vec4 color
)
{
    context.color = color;

    for (const PerfTimeline::Interval &interval : event.intervals())
    {
        context.drawRect(
            x, bottomY - interval.threadEnd / graphTimeResolution,
            1, (interval.threadEnd - interval.threadStart) / graphTimeResolution
        );
    }
}

void drawPerfHistoryGraph(
    GraphicsContext &context,
    PerfHistory &history,
    int x, int y,
    int width, int height,
    float alpha
)
{
    int markerSpacing = markerPeriod / graphTimeResolution;

    std::chrono::duration timeOffset = markerPeriod;
    context.color = glm::vec4(0.5f, 0.5f, 0.5f, alpha);

    for (int plotY=y+height-markerSpacing; plotY>=y; plotY-=markerSpacing)
    {
        std::string label = formatString("$+ms", timeOffset / 1ms);
        context.drawRect(x, plotY, width, 1);
        context.drawText(Font::generic, x + 20, plotY + 1, label);
        timeOffset += markerPeriod;
    }

#ifndef NO_PERF_TIMERS
    if (settings.frameDumpThresholdMS.has_value())
    {
        int thresholdInPixels = (*settings.frameDumpThresholdMS * 1ms) / graphTimeResolution;
        int thresholdY = y + height - thresholdInPixels;

        context.color = glm::vec4(1.0f, 1.0f, 0.0f, alpha);
        context.drawRect(x, thresholdY, width, 1);
    }
#endif

    for (int offset=-1; offset>=-width; offset--)
    {
        int plotX = width + x + offset;
        int bottomY = y + height;

        PerfTimeline &timeline = history.getTimeline(offset);

        drawEvent(context, timeline.drawFrame, plotX, bottomY, glm::vec4(0.05, 0.5f, 0.7f, alpha));
        drawEventThreadTime(context, timeline.drawFrame, plotX, bottomY, glm::vec4(0.19f, 0.8f, 0.19f, alpha));
    }
}

static void drawTimelineEvent(PixelMap &chart, PerfTimeline &timeline, int y, const PerfTimeline::Event &event, glm::vec3 color)
{
    PixelMap::Pixel pixel{
        (uint8_t) (color.x * 255.0f),
        (uint8_t) (color.y * 255.0f),
        (uint8_t) (color.z * 255.0f),
        0xFF
    };

    for (const PerfTimeline::Interval &interval : event.intervals())
    {
        int startX = interval.start / chartTimeResolution;
        float fwidth = (interval.end - interval.start) / chartTimeResolution;
        int width = std::max(1, (int) std::ceil(fwidth));

        chart.fillRect(startX, y, width, chartBarHeight, pixel);
    }
}

static void drawTimelineEventTotal(PixelMap &chart, PerfTimeline &timeline, int y, const PerfTimeline::Event &event, glm::vec3 color)
{
    PixelMap::Pixel pixel{
        (uint8_t) (color.x * 255.0f),
        (uint8_t) (color.y * 255.0f),
        (uint8_t) (color.z * 255.0f),
        0xFF
    };

    PerfTimeline::Ticks total(0);
    for (const PerfTimeline::Interval &interval : event.intervals())
    {
        total += interval.end - interval.start;
    }

    float fwidth = total / chartTimeResolution;
    int width = (int) std::ceil(fwidth);

    chart.fillRect(0, y, width, chartBarHeight, pixel);
}

PixelMap makePerfTimelineChart(PerfTimeline &timeline, PerfTimeline::Ticks threshold)
{
    static constexpr int spacingBetweenCharts = 15;

    PerfTimeline::Ticks frameDuration = timeline.getCurrentDuration();

    int width = frameDuration / chartTimeResolution;
    width += 1;

    int height = spacingBetweenCharts
        #define DECL_PERF_EVENT(...) + 2 * chartBarHeight
        #include <PerfTimeline/PerfEventList.hpp>
    ;

    PixelMap chart(width, height, PixelMap::Pixel{0, 0, 0, 0xFF});

    int plotY = 0;

    #define DECL_PERF_EVENT(name, label, red, green, blue) \
        drawTimelineEvent(chart, timeline, plotY, timeline.name, glm::vec3(red, green, blue)); \
        plotY += chartBarHeight;
    #include <PerfTimeline/PerfEventList.hpp>

    chart.fillRect(0, plotY + 5, width, spacingBetweenCharts - 10, PixelMap::Pixel{0x7F, 0x7F, 0x07F, 0xFF});
    plotY += spacingBetweenCharts;

    #define DECL_PERF_EVENT(name, label, red, green, blue) \
        drawTimelineEventTotal(chart, timeline, plotY, timeline.name, glm::vec3(red, green, blue)); \
        plotY += chartBarHeight;
    #include <PerfTimeline/PerfEventList.hpp>

    int markerSpacing = 1ms / chartTimeResolution;
    for (int ms=1; markerSpacing*ms<width; ms++)
    {
        static PixelMap::Pixel marker1{0x44, 0x44, 0x44, 0xFF};
        static PixelMap::Pixel marker5{0xAA, 0xAA, 0xAA, 0xFF};

        chart.fillRect(markerSpacing*ms, 0, 1, height, (ms % 5) ? marker1 : marker5);
    }

    int thresholdX = threshold / chartTimeResolution;
    chart.fillRect(thresholdX, 0, 1, height, PixelMap::Pixel{0xFF, 0xFF, 0x00, 0xFF});

    return chart;
}