#include "SurfaceState.hpp"
#include "helpers/Format.hpp"
#include "protocols/types/Buffer.hpp"
#include "protocols/PresentationTime.hpp"
#include "managers/eventLoop/EventLoopManager.hpp"
#include "render/Renderer.hpp"
#include "render/Texture.hpp"

Vector2D SSurfaceState::sourceSize() {
    if UNLIKELY (!texture)
        return {};

    if UNLIKELY (viewport.hasSource)
        return viewport.source.size();

    Vector2D trc = transform % 2 == 1 ? Vector2D{bufferSize.y, bufferSize.x} : bufferSize;
    return trc / scale;
}

bool SSurfaceState::consumeBufferDamage() const {
    return buffer && !buffer->isSynchronous();
}

CRegion SSurfaceState::accumulateBufferDamage() {
    if (!damage.empty()) {
        CRegion surfaceDamage = damage;
        if (viewport.hasDestination) {
            Vector2D scale = sourceSize() / viewport.destination;
            surfaceDamage.scale(scale);
        }

        if (viewport.hasSource)
            surfaceDamage.translate(viewport.source.pos());

        Vector2D trc = transform % 2 == 1 ? Vector2D{bufferSize.y, bufferSize.x} : bufferSize;

        bufferDamage = surfaceDamage.scale(scale).transform(Math::wlTransformToHyprutils(Math::invertTransform(transform)), trc.x, trc.y).add(bufferDamage);
        damage.clear();
    }

    auto taken = bufferDamage;
    if (consumeBufferDamage())
        bufferDamage.clear();

    return taken;
}

CRegion SSurfaceState::effectiveInputRegion() const {
    if (size.x <= 0 || size.y <= 0)
        return {};

    if (inputIsInfinite)
        return CBox{{}, size};

    return input.copy().intersect(CBox{{}, size});
}

void SSurfaceState::updateSynchronousTexture(SP<Render::ITexture> lastTexture) {
    auto [dataPtr, fmt, size] = buffer->beginDataPtr(0);
    if (dataPtr) {
        auto drmFmt = NFormatUtils::shmToDRM(fmt);
        auto stride = bufferSize.y ? size / bufferSize.y : 0;
        if (lastTexture && lastTexture->m_isSynchronous && lastTexture->m_size == bufferSize) {
            texture = lastTexture;
            texture->update(drmFmt, dataPtr, stride, accumulateBufferDamage());
        } else
            texture = g_pHyprRenderer->createTexture(drmFmt, dataPtr, stride, bufferSize);
    }
    buffer->endDataPtr();
}

void SSurfaceState::reset() {
    updated.all = false;

    // After commit, there is no pending buffer until the next attach.
    buffer = {};

    // applies only to the buffer that is attached to the surface
    acquire = {};

    // wl_surface.commit assigns pending ... and clears pending damage.
    damage.clear();
    bufferDamage.clear();

    callbacks.clear();
    presentationFeedbacks.clear();
    lockMask = LOCK_REASON_NONE;

    barrierSet            = false;
    barrierWait           = false;
    waitingOnPresentation = false;

    pendingTimeout.reset();
    commitTimingTarget.reset();
    timer.reset(); // CEventLoopManager::nudgeTimers should handle it eventually
}

bool SSurfaceState::isLocked() const {
    return lockMask != LOCK_REASON_NONE;
}

bool SSurfaceState::fenceSignaled() const {
    if (buffer) {
        for (const auto& fd : buffer->m_syncFds) {
            if (!fd.isReadable())
                return false;
        }
    }

    if (acquireWaiter && !acquireWaiter->fd.isReadable())
        return false;

    return true;
}

void SSurfaceState::cancelFenceWaiter() {
    if (acquireWaiter && g_pEventLoopManager)
        g_pEventLoopManager->removeReadableWaiter(acquireWaiter);
    acquireWaiter.reset();
}

void SSurfaceState::mergeFrom(SSurfaceState& ref) {
    updated.all |= ref.updated.all;

    if (ref.updated.bits.buffer) {
        if (!presentationFeedbacks.empty())
            PROTO::presentation->discardFeedbacks(presentationFeedbacks);

        buffer     = ref.buffer;
        texture    = ref.texture;
        size       = ref.size;
        bufferSize = ref.bufferSize;
    }

    if (ref.updated.bits.damage) {
        damage.add(ref.damage);
        bufferDamage.add(ref.bufferDamage);
    }

    if (ref.updated.bits.input) {
        input           = ref.input;
        inputIsInfinite = ref.inputIsInfinite;
    }

    if (ref.updated.bits.opaque)
        opaque = ref.opaque;

    if (ref.updated.bits.offset)
        offset = ref.offset;

    if (ref.updated.bits.scale)
        scale = ref.scale;

    if (ref.updated.bits.transform)
        transform = ref.transform;

    if (ref.updated.bits.viewport)
        viewport = ref.viewport;

    if (ref.updated.bits.acquire)
        acquire = ref.acquire;

    if (ref.updated.bits.acked)
        ackedSize = ref.ackedSize;

    if (ref.updated.bits.frame) {
        callbacks.insert(callbacks.end(), std::make_move_iterator(ref.callbacks.begin()), std::make_move_iterator(ref.callbacks.end()));
        ref.callbacks.clear();
    }

    if (ref.updated.bits.presentation) {
        presentationFeedbacks.insert(presentationFeedbacks.end(), std::make_move_iterator(ref.presentationFeedbacks.begin()),
                                     std::make_move_iterator(ref.presentationFeedbacks.end()));
        ref.presentationFeedbacks.clear();
    }
}

void SSurfaceState::updateFrom(SSurfaceState& ref) {
    updated = ref.updated;

    if (ref.updated.bits.buffer) {
        if (!presentationFeedbacks.empty())
            PROTO::presentation->discardFeedbacks(presentationFeedbacks);

        buffer     = ref.buffer;
        texture    = ref.texture;
        size       = ref.size;
        bufferSize = ref.bufferSize;
    }

    if (ref.updated.bits.damage) {
        if (consumeBufferDamage()) {
            // what we still hold is only consumed on rendering.
            // dropping it here would leave those pixels stale.
            damage.add(ref.damage);
            bufferDamage.add(ref.bufferDamage);
        } else {
            damage       = ref.damage;
            bufferDamage = ref.bufferDamage;
        }
    } else if (ref.updated.bits.buffer || !consumeBufferDamage()) {
        // damage is relative to the buffer, shm drops the buffer on commit
        // dmabuf doesnt drop it until we recieve a new one. and we cant clear
        // the dmabuf damage until the renderer has actually consumed it.
        damage.clear();
        bufferDamage.clear();
    }

    if (ref.updated.bits.input) {
        input           = ref.input;
        inputIsInfinite = ref.inputIsInfinite;
    }

    if (ref.updated.bits.opaque)
        opaque = ref.opaque;

    if (ref.updated.bits.offset)
        offset = ref.offset;

    if (ref.updated.bits.scale)
        scale = ref.scale;

    if (ref.updated.bits.transform)
        transform = ref.transform;

    if (ref.updated.bits.viewport)
        viewport = ref.viewport;

    if (ref.updated.bits.acquire)
        acquire = ref.acquire;

    if (ref.updated.bits.acked)
        ackedSize = ref.ackedSize;

    if (ref.updated.bits.frame) {
        callbacks.insert(callbacks.end(), std::make_move_iterator(ref.callbacks.begin()), std::make_move_iterator(ref.callbacks.end()));
        ref.callbacks.clear();
    }

    if (ref.updated.bits.presentation) {
        presentationFeedbacks.insert(presentationFeedbacks.end(), std::make_move_iterator(ref.presentationFeedbacks.begin()),
                                     std::make_move_iterator(ref.presentationFeedbacks.end()));
        ref.presentationFeedbacks.clear();
    }

    if (ref.barrierSet)
        waitingOnPresentation = true;
}
