#include "ScrollTapeController.hpp"
#include "ScrollingAlgorithm.hpp"
#include "../../../../config/ConfigValue.hpp"
#include <algorithm>
#include <cmath>

using namespace Layout::Tiled;

CScrollTapeController::CScrollTapeController(eScrollDirection direction) : m_direction(direction) {
    ;
}

void CScrollTapeController::setDirection(eScrollDirection dir) {
    m_direction = dir;
}

eScrollDirection CScrollTapeController::getDirection() const {
    return m_direction;
}

bool CScrollTapeController::isPrimaryHorizontal() const {
    return m_direction == SCROLL_DIR_RIGHT || m_direction == SCROLL_DIR_LEFT;
}

bool CScrollTapeController::isReversed() const {
    return m_direction == SCROLL_DIR_LEFT || m_direction == SCROLL_DIR_UP;
}

size_t CScrollTapeController::stripCount() const {
    return m_strips.size();
}

SStripData& CScrollTapeController::getStrip(size_t index) {
    return m_strips[index];
}

const SStripData& CScrollTapeController::getStrip(size_t index) const {
    return m_strips[index];
}

void CScrollTapeController::setOffset(double offset) {
    if (getScrollInhibitor().isInhibited) {
        m_offset = getScrollInhibitor().offsetWhenInhibited;
        Log::logger->log(Log::DEBUG, "m_offset not set - scrolling inhibited");
    } else
        m_offset = offset;
}

double CScrollTapeController::getOffset() const {
    return m_offset;
}

void CScrollTapeController::adjustOffset(double delta) {
    m_offset += delta;
}

struct SScrollInhibitor& CScrollTapeController::getScrollInhibitor() {
    return m_scrollInhibitor;
}

size_t CScrollTapeController::addStrip(float size) {
    m_strips.emplace_back();
    m_strips.back().size = size;
    return m_strips.size() - 1;
}

void CScrollTapeController::insertStrip(ssize_t afterIndex, float size) {
    if (afterIndex >= sc<ssize_t>(m_strips.size())) {
        addStrip(size);
        return;
    }

    afterIndex = std::clamp(afterIndex, sc<ssize_t>(-1L), sc<ssize_t>(INT32_MAX));

    SStripData newStrip;
    newStrip.size = size;
    m_strips.insert(m_strips.begin() + afterIndex + 1, newStrip);
}

void CScrollTapeController::removeStrip(size_t index) {
    if (index < m_strips.size())
        m_strips.erase(m_strips.begin() + index);
}

double CScrollTapeController::getPrimary(const Vector2D& v) const {
    return isPrimaryHorizontal() ? v.x : v.y;
}

double CScrollTapeController::getSecondary(const Vector2D& v) const {
    return isPrimaryHorizontal() ? v.y : v.x;
}

void CScrollTapeController::setPrimary(Vector2D& v, double val) const {
    if (isPrimaryHorizontal())
        v.x = val;
    else
        v.y = val;
}

void CScrollTapeController::setSecondary(Vector2D& v, double val) const {
    if (isPrimaryHorizontal())
        v.y = val;
    else
        v.x = val;
}

Vector2D CScrollTapeController::makeVector(double primary, double secondary) const {
    if (isPrimaryHorizontal())
        return {primary, secondary};
    else
        return {secondary, primary};
}

double CScrollTapeController::calculateMaxExtent(const CBox& usableArea, bool fullscreenOnOne) const {
    if (m_strips.empty())
        return 0.0;

    if (fullscreenOnOne && m_strips.size() == 1)
        return getPrimary(usableArea.size());

    double       total         = 0.0;
    const double usablePrimary = getPrimary(usableArea.size());

    for (const auto& strip : m_strips) {
        total += usablePrimary * strip.size;
    }

    return total;
}

double CScrollTapeController::calculateStripStart(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) const {
    if (stripIndex >= m_strips.size())
        return 0.0;

    const double usablePrimary = getPrimary(usableArea.size());
    double       current       = 0.0;

    for (size_t i = 0; i < stripIndex; ++i) {
        const double stripSize = (fullscreenOnOne && m_strips.size() == 1) ? usablePrimary : usablePrimary * m_strips[i].size;
        current += stripSize;
    }

    return current;
}

double CScrollTapeController::calculateStripSize(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) const {
    if (stripIndex >= m_strips.size())
        return 0.0;

    const double usablePrimary = getPrimary(usableArea.size());

    if (fullscreenOnOne && m_strips.size() == 1)
        return usablePrimary;

    return usablePrimary * m_strips[stripIndex].size;
}

CBox CScrollTapeController::calculateStripBox(size_t stripIndex, const CBox& usableArea, const Vector2D& workspaceOffset, bool fullscreenOnOne) {
    if (stripIndex >= m_strips.size())
        return {};

    const double usableSecondary = getSecondary(usableArea.size());
    const double usablePrimary   = getPrimary(usableArea.size());
    const double cameraOffset    = calculateCameraOffset(usableArea, fullscreenOnOne);

    // calculate position along primary axis (strip position)
    double primaryPos  = calculateStripStart(stripIndex, usableArea, fullscreenOnOne);
    double primarySize = calculateStripSize(stripIndex, usableArea, fullscreenOnOne);

    // apply camera offset based on direction
    // for RIGHT/DOWN: scroll offset moves content left/up (subtract)
    // for LEFT/UP: scroll offset moves content right/down (different coordinate system)
    if (m_direction == SCROLL_DIR_LEFT) {
        // LEFT: flip the entire primary axis, then apply offset
        primaryPos = usablePrimary - primaryPos - primarySize + cameraOffset;
    } else if (m_direction == SCROLL_DIR_UP) {
        // UP: flip the entire primary axis, then apply offset
        primaryPos = usablePrimary - primaryPos - primarySize + cameraOffset;
    } else {
        // RIGHT/DOWN: normal offset
        primaryPos -= cameraOffset;
    }

    // create the box in primary/secondary coordinates
    Vector2D pos  = makeVector(primaryPos, 0.0);
    Vector2D size = makeVector(primarySize, usableSecondary);

    // translate to workspace position
    pos = pos + workspaceOffset;

    return CBox{pos, size};
}

CBox CScrollTapeController::calculateTargetBox(size_t stripIndex, size_t targetIndex, const CBox& usableArea, const Vector2D& workspaceOffset, bool fullscreenOnOne) {
    if (stripIndex >= m_strips.size())
        return {};

    const auto& strip = m_strips[stripIndex];
    if (targetIndex >= strip.targetSizes.size())
        return {};

    CBox         stripBox        = calculateStripBox(stripIndex, usableArea, workspaceOffset, fullscreenOnOne);
    const double usableSecondary = getSecondary(usableArea.size());

    double       secondaryPos = 0.0;
    for (size_t i = 0; i < targetIndex; ++i) {
        secondaryPos += strip.targetSizes[i] * usableSecondary;
    }

    const double secondarySize = strip.targetSizes[targetIndex] * usableSecondary;

    if (isPrimaryHorizontal()) {
        stripBox.y = workspaceOffset.y + secondaryPos;
        stripBox.h = secondarySize;
    } else {
        stripBox.x = workspaceOffset.x + secondaryPos;
        stripBox.w = secondarySize;
    }

    return stripBox;
}

double CScrollTapeController::calculateCameraOffset(const CBox& usableArea, bool fullscreenOnOne) {
    const double maxExtent     = calculateMaxExtent(usableArea, fullscreenOnOne);
    const double usablePrimary = getPrimary(usableArea.size());

    // don't adjust the offset if we are dragging
    if (isBeingDragged())
        return m_offset;

    // if the content fits in viewport, center it
    if (maxExtent < usablePrimary)
        setOffset(std::round((maxExtent - usablePrimary) / 2.0));

    // if the offset is negative but we already extended and fit method is not center, reset offset to 0
    static const auto PFITMETHOD = CConfigValue<Hyprlang::INT>("scrolling:focus_fit_method");
    if (maxExtent > usablePrimary && m_offset < 0.0 && *PFITMETHOD != 0)
        setOffset(0.0);

    return m_offset;
}

Vector2D CScrollTapeController::getCameraTranslation(const CBox& usableArea, bool fullscreenOnOne) {
    const double offset = calculateCameraOffset(usableArea, fullscreenOnOne);

    if (isReversed())
        return makeVector(offset, 0.0);
    else
        return makeVector(-offset, 0.0);
}

void CScrollTapeController::centerStrip(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) {
    if (stripIndex >= m_strips.size())
        return;

    const double usablePrimary = getPrimary(usableArea.size());
    const double stripStart    = calculateStripStart(stripIndex, usableArea, fullscreenOnOne);
    const double stripSize     = calculateStripSize(stripIndex, usableArea, fullscreenOnOne);

    setOffset(stripStart - (usablePrimary - stripSize) / 2.0);
}

void CScrollTapeController::fitStrip(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) {
    if (stripIndex >= m_strips.size())
        return;

    const double usablePrimary = getPrimary(usableArea.size());
    const double stripStart    = calculateStripStart(stripIndex, usableArea, fullscreenOnOne);
    const double stripSize     = calculateStripSize(stripIndex, usableArea, fullscreenOnOne);

    const double lo = stripStart - usablePrimary + stripSize;
    const double hi = stripStart;

    if (lo > hi) {
        // strip is wider than viewport (e.g. during monitor reconnection after suspend),
        // center the strip instead of hitting the std::clamp assertion
        setOffset(stripStart - (usablePrimary - stripSize) / 2.0);
        return;
    }

    setOffset(std::clamp(m_offset, lo, hi));
}

bool CScrollTapeController::isStripVisible(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne, bool full) const {
    if (stripIndex >= m_strips.size())
        return false;

    const double stripStart = calculateStripStart(stripIndex, usableArea, fullscreenOnOne);
    const double stripEnd   = stripStart + calculateStripSize(stripIndex, usableArea, fullscreenOnOne);
    const double viewStart  = m_offset;
    const double viewEnd    = m_offset + getPrimary(usableArea.size());

    if (!full)
        return stripStart < viewEnd && viewStart < stripEnd;
    else
        return stripStart >= viewStart && stripEnd <= viewEnd;
}

size_t CScrollTapeController::getStripAtCenter(const CBox& usableArea, bool fullscreenOnOne) const {
    if (m_strips.empty())
        return 0;

    const double usablePrimary = getPrimary(usableArea.size());
    const double viewCenter    = m_offset + usablePrimary / 2.0;
    double       currentEnd    = 0.0;

    for (size_t i = 0; i < m_strips.size(); ++i) {
        const double stripSize = calculateStripSize(i, usableArea, fullscreenOnOne);
        currentEnd += stripSize;

        if (currentEnd >= viewCenter - 2.0)
            return i;
    }

    return m_strips.empty() ? 0 : m_strips.size() - 1;
}

void CScrollTapeController::swapStrips(size_t a, size_t b) {
    if (a >= m_strips.size() || b >= m_strips.size())
        return;

    std::swap(m_strips.at(a), m_strips.at(b));
}

bool CScrollTapeController::isBeingDragged() const {
    for (const auto& s : m_strips) {
        if (!s.userData)
            continue;

        for (const auto& d : s.userData->targetDatas) {
            if (d->target == g_layoutManager->dragController()->target())
                return true;
        }
    }

    return false;
}
