// param_slider.cpp — see param_slider.h. Pure control-surface geometry; no host types.

#include "core/instrument/ui/param_slider.h"

#include "core/util/clamp01.h"

#include <algorithm>
#include <cmath>

namespace reasampler::instrument::ui {

using util::clamp01;

std::vector<ControlRow> layoutControls(const Rect& panel,
                                       const std::vector<ControlDesc>& controls) {
    std::vector<ControlRow> out;
    if (controls.empty() || panel.width <= 0 || panel.height <= 0) return out;
    out.reserve(controls.size());

    // The label column is clamped so a narrow panel still leaves a control column.
    const int labelW = (std::min)(kControlLabelWidth, (std::max)(0, panel.width / 2));
    int rowTop = panel.y;
    for (const ControlDesc& d : controls) {
        ControlRow r;
        r.id = d.id;
        r.kind = d.kind;
        const int rowBottom = rowTop + kControlRowHeight;
        r.row = Rect::ltrb(panel.x, rowTop, panel.right(), rowBottom);
        r.label = Rect::ltrb(panel.x, rowTop, panel.x + labelW, rowBottom);
        r.control = Rect::ltrb(panel.x + labelW, rowTop, panel.right(), rowBottom);
        out.push_back(r);
        rowTop = rowBottom + kControlRowGap;
    }
    return out;
}

Rect toggleSegmentRect(const Rect& control, int seg) {
    if (seg < 0 || seg >= kToggleSegments) return Rect{};
    const int w = control.width;
    if (w <= 0 || control.height <= 0) return Rect{};
    const int segW = w / kToggleSegments;
    const int left = control.x + seg * segW;
    // The last segment absorbs the width remainder so the segments tile the whole control.
    const int right = (seg == kToggleSegments - 1) ? control.right() : left + segW;
    return Rect::ltrb(left, control.y, right, control.bottom());
}

int toggleSegmentHitTest(const Rect& control, int x, int y) {
    if (!contains(control, x, y)) return -1;
    for (int seg = 0; seg < kToggleSegments; ++seg) {
        if (contains(toggleSegmentRect(control, seg), x, y)) return seg;
    }
    return -1;
}

Rect sliderTrackRect(const Rect& control) {
    // Inset a half-handle at each end so the handle stays fully inside the control at value
    // 0 and 1. The handle CENTER ranges across [track.x, track.right()].
    const int half = kSliderHandleWidth / 2;
    if (control.width <= kSliderHandleWidth || control.height <= 0) return Rect{};
    return Rect::ltrb(control.x + half, control.y, control.right() - half, control.bottom());
}

Rect sliderHandleRect(const Rect& control, double value) {
    const Rect track = sliderTrackRect(control);
    if (track.width <= 0) return Rect{};
    if (value < 0.0) value = 0.0;
    if (value > 1.0) value = 1.0;
    const int span = track.width;  // handle-center movable span
    const int centerX = track.x + static_cast<int>(value * span + 0.5);
    const int half = kSliderHandleWidth / 2;
    return Rect::ltrb(centerX - half, control.y, centerX - half + kSliderHandleWidth,
                control.bottom());
}

double valueAtPoint(const Rect& control, int x) {
    const Rect track = sliderTrackRect(control);
    const int span = track.width;
    if (span <= 0) return 0.0;
    if (x <= track.x) return 0.0;
    if (x >= track.right()) return 1.0;
    return static_cast<double>(x - track.x) / static_cast<double>(span);
}

// --- Radial knob -----------------------------------------------------------------------

namespace {

constexpr double kPi = 3.14159265358979323846;

// Normalize an angle in degrees to [0, 360).
double normDeg(double deg) {
    deg = std::fmod(deg, 360.0);
    if (deg < 0.0) deg += 360.0;
    // Guard: fmod can return exactly 360.0 on some implementations due to floating-point
    // rounding; fold it back to 0.
    if (deg >= 360.0) deg -= 360.0;
    return deg;
}

}  // namespace

KnobGeometry computeKnob(const Rect& cell) {
    if (cell.width <= 0 || cell.height <= 0) return KnobGeometry{};
    KnobGeometry g;
    g.centerX = (cell.x + cell.right()) / 2.0;
    g.centerY = (cell.y + cell.bottom()) / 2.0;
    g.radius = (std::min)(cell.width, cell.height) / 2.0;
    return g;
}

bool knobHitTest(const KnobGeometry& knob, int x, int y) {
    if (knob.radius <= 0.0) return false;
    const double dx = x - knob.centerX;
    const double dy = y - knob.centerY;
    // Boundary exclusive: matches the module's half-open Rect convention.
    return dx * dx + dy * dy < knob.radius * knob.radius;
}

double knobSweepDeg(const KnobArc& arc) {
    const double sweep = normDeg(arc.endDeg) - normDeg(arc.startDeg);
    // An end at-or-behind the start wraps clockwise past 12 o'clock; equal angles mean a
    // full circle.
    return sweep <= 0.0 ? sweep + 360.0 : sweep;
}

double knobValueAngleDeg(const KnobArc& arc, double value) {
    return normDeg(normDeg(arc.startDeg) + knobSweepDeg(arc) * clamp01(value));
}

KnobPoint knobNeedlePoint(const KnobGeometry& knob, const KnobArc& arc, double value) {
    // Clock angle -> screen direction: 0° points up (-y), 90° points right (+x).
    const double rad = knobValueAngleDeg(arc, value) * kPi / 180.0;
    return KnobPoint{knob.centerX + knob.radius * std::sin(rad),
                     knob.centerY - knob.radius * std::cos(rad)};
}

double knobDragValue(double startValue, int dyPixels, const DragModifiers& mods,
                     int dragRangePixels) {
    const double start = clamp01(startValue);
    if (dragRangePixels <= 0) return start;
    const double dy = static_cast<double>(dyPixels) * (fineDrag(mods) ? kFineDragScale : 1.0);
    // Screen y grows downward: an upward drag (negative dy) increases the value.
    return clamp01(start - dy / dragRangePixels);
}

int controlAtPoint(const std::vector<ControlRow>& rows, int x, int y) {
    for (const ControlRow& r : rows) {
        if (r.kind == ControlKind::Toggle) {
            if (contains(r.control, x, y)) return r.id;
        } else if (r.kind == ControlKind::Knob) {
            if (knobHitTest(computeKnob(r.control), x, y)) return r.id;
        } else {  // Slider — the interactive area is the track
            if (contains(sliderTrackRect(r.control), x, y)) return r.id;
        }
    }
    return -1;
}

}  // namespace reasampler::instrument::ui
