#include "gdi_overlay.h"

#include <cstddef>

#include "util/logging.h"

namespace {

    // the back buffer matches the window DC; the software buffer has a fixed 32-bit layout
    enum class BufferType {
        TargetCompatible,
        Bgra32,
    };

    struct GdiBuffer {
        HDC dc = nullptr;
        HBITMAP bitmap = nullptr;
        // bitmap originally selected into the memory DC, restored before cleanup
        HGDIOBJ old_bitmap = nullptr;
        int width = 0;
        int height = 0;
    };

    // back buffer holds the complete frame; overlay buffer holds ImGui software pixels
    GdiBuffer BACK_BUFFER;
    GdiBuffer OVERLAY_BUFFER;

    void release_buffer(GdiBuffer &buffer) {
        // destroy the DC before the bitmap so cleanup is safe even if restoration fails
        if (buffer.dc != nullptr) {
            if (buffer.old_bitmap != nullptr && buffer.old_bitmap != HGDI_ERROR) {
                SelectObject(buffer.dc, buffer.old_bitmap);
            }
            DeleteDC(buffer.dc);
        }
        if (buffer.bitmap != nullptr) {
            DeleteObject(buffer.bitmap);
        }

        buffer = {};
    }

    // ensures the buffer has a memory DC with a bitmap of the requested size and type
    // selected into it. a matching allocation is reused; otherwise the old resources are
    // released and recreated. returns false if the dimensions or any GDI operation fail.
    bool ensure_buffer(
            GdiBuffer &buffer,
            HDC target_dc,
            int width,
            int height,
            BufferType type,
            const char *name) {
        if (width <= 0 || height <= 0) {
            return false;
        }
        if (buffer.dc != nullptr && buffer.bitmap != nullptr &&
            buffer.width == width && buffer.height == height) {
            return true;
        }

        // keep allocations across frames and recreate only after a size change
        release_buffer(buffer);
        buffer.dc = CreateCompatibleDC(target_dc);
        if (buffer.dc == nullptr) {
            log_warning("touch", "failed to create {} DC: {}", name, GetLastError());
            return false;
        }

        // compatible bitmaps are fast presentation targets; BGRA bitmaps accept raw pixels
        if (type == BufferType::TargetCompatible) {
            buffer.bitmap = CreateCompatibleBitmap(target_dc, width, height);
        } else {
            buffer.bitmap = CreateBitmap(width, height, 1, sizeof(uint32_t) * 8, nullptr);
        }
        if (buffer.bitmap == nullptr) {
            log_warning("touch", "failed to create {} bitmap: {}", name, GetLastError());
            release_buffer(buffer);
            return false;
        }

        buffer.old_bitmap = SelectObject(buffer.dc, buffer.bitmap);
        if (buffer.old_bitmap == nullptr || buffer.old_bitmap == HGDI_ERROR) {
            log_warning("touch", "failed to select {} bitmap: {}", name, GetLastError());
            release_buffer(buffer);
            return false;
        }

        buffer.width = width;
        buffer.height = height;
        return true;
    }

    bool update_overlay_buffer(
            HDC target_dc,
            const uint32_t *pixels,
            bool pixels_dirty,
            int width,
            int height) {
        if (pixels == nullptr) {
            return false;
        }

        bool needs_update = pixels_dirty || OVERLAY_BUFFER.bitmap == nullptr ||
            OVERLAY_BUFFER.width != width || OVERLAY_BUFFER.height != height;
        if (!ensure_buffer(
                OVERLAY_BUFFER,
                target_dc,
                width,
                height,
                BufferType::Bgra32,
                "software overlay")) {
            return false;
        }
        if (!needs_update) {
            return true;
        }

        // SetDIBits requires the destination bitmap not to be selected into a DC
        HGDIOBJ overlay_bitmap =
            SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.old_bitmap);
        if (overlay_bitmap == nullptr || overlay_bitmap == HGDI_ERROR) {
            log_warning("touch", "failed to deselect software overlay bitmap: {}", GetLastError());
            release_buffer(OVERLAY_BUFFER);
            return false;
        }

        BITMAPINFO bitmap_info {};
        bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
        bitmap_info.bmiHeader.biWidth = width;
        bitmap_info.bmiHeader.biHeight = -height;
        bitmap_info.bmiHeader.biPlanes = 1;
        bitmap_info.bmiHeader.biBitCount = sizeof(uint32_t) * 8;
        bitmap_info.bmiHeader.biCompression = BI_RGB;

        int copied_lines = SetDIBits(
            target_dc,
            OVERLAY_BUFFER.bitmap,
            0,
            height,
            pixels,
            &bitmap_info,
            DIB_RGB_COLORS);

        HGDIOBJ old_bitmap = SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.bitmap);
        if (old_bitmap == nullptr || old_bitmap == HGDI_ERROR) {
            log_warning("touch", "failed to reselect software overlay bitmap: {}", GetLastError());
            release_buffer(OVERLAY_BUFFER);
            return false;
        }
        OVERLAY_BUFFER.old_bitmap = old_bitmap;

        if (copied_lines != height) {
            log_warning("touch", "failed to update software overlay bitmap: {} of {} lines copied",
                        copied_lines, height);
            release_buffer(OVERLAY_BUFFER);
            return false;
        }
        return true;
    }
}

HDC touch_gdi_overlay_begin_frame(
        HDC target_dc,
        HBRUSH background_brush,
        int width,
        int height,
        const uint32_t *overlay_pixels,
        bool overlay_pixels_dirty,
        int overlay_width,
        int overlay_height) {
    if (!ensure_buffer(
            BACK_BUFFER,
            target_dc,
            width,
            height,
            BufferType::TargetCompatible,
            "overlay back buffer")) {
        return nullptr;
    }

    HDC draw_dc = BACK_BUFFER.dc;
    SetBkMode(draw_dc, TRANSPARENT);

    // start each frame from the transparent color-key background
    RECT buffer_rect {0, 0, width, height};
    FillRect(draw_dc, &buffer_rect, background_brush);

    if (update_overlay_buffer(
            target_dc,
            overlay_pixels,
            overlay_pixels_dirty,
            overlay_width,
            overlay_height) &&
        !BitBlt(draw_dc, 0, 0, overlay_width, overlay_height,
                OVERLAY_BUFFER.dc, 0, 0, SRCCOPY)) {
        log_warning("touch", "failed to draw software overlay bitmap: {}", GetLastError());
    }

    return draw_dc;
}

void touch_gdi_overlay_present(HDC target_dc) {
    // one full-window blit exposes the completed frame without an intermediate erase
    if (!BitBlt(target_dc, 0, 0, BACK_BUFFER.width, BACK_BUFFER.height,
                BACK_BUFFER.dc, 0, 0, SRCCOPY)) {
        log_warning("touch", "failed to present overlay back buffer: {}", GetLastError());
    }
}

void touch_gdi_overlay_release() {
    release_buffer(BACK_BUFFER);
    release_buffer(OVERLAY_BUFFER);
}
