// stroke_aa.h — analytic antialiased thick-stroke coverage: distance-to-polyline, MAX-accumulated
// into a scratch mask that a shell blends ONCE. Pure geometry; no LICE, no host types.
//
// The single blend is the load-bearing part. Compositing a stroke segment-by-segment (or as a
// stack of 1px arcs) re-lays ink over the previous segment's antialiased fringe, which is what
// makes a stroke read as a soft glow that never reaches an opaque core.

#pragma once

#include <cstddef>
#include <vector>

#include "core/ui/rect.h"

namespace reasampler::ui {

struct StrokePoint {
    float x = 0.0f;
    float y = 0.0f;
};

// Chord-flatness bound for arc flattening: an order of magnitude under the AA fringe the arc is
// drawn with, so the polyline is indistinguishable from the true arc at any radius the deck uses.
inline constexpr float kArcFlatnessPx = 0.05f;

// Hard cap on flattening output, so a pathological radius/flatness pair terminates with a coarse
// arc rather than allocating without bound.
inline constexpr int kMaxArcSegments = 512;

// Long segments are split before rasterizing. This is EXACT, not an approximation: the minimum
// distance to a partition of a segment is the minimum distance to the whole segment. It exists
// because a piece is rasterized over its bounding box, and one long diagonal's box has area
// O(len^2) — subdivision is what keeps a stroke's cost linear in its length.
inline constexpr float kMaxPieceLen = 4.0f;

// A reusable coverage mask. Allocation is amortized across calls: `reset` grows the buffer but
// never clears it, because per-row valid extents make a clear unnecessary.
class StrokeCanvas {
   public:
    // Grows the buffer to fit `bounds` and marks every row empty. O(height), not O(area).
    void reset(const Rect& bounds);

    // MAX-accumulates one segment. Endpoints are round-capped, so a zero-length segment is a dot
    // of radius `halfWidth` and a polyline's joints are round by construction.
    void addSegment(float ax, float ay, float bx, float by, float halfWidth);

    const Rect& bounds() const { return bounds_; }

    // Only [rowLo, rowHi) of a row holds meaningful coverage; outside that span the buffer is
    // deliberately uninitialized, which is what keeps cost proportional to ink, not to the box.
    int rowLo(int y) const { return rowLo_[static_cast<std::size_t>(y - bounds_.y)]; }
    int rowHi(int y) const { return rowHi_[static_cast<std::size_t>(y - bounds_.y)]; }

    // Row base pointer; index it by (x - bounds().x) within [rowLo, rowHi).
    const float* rowData(int y) const {
        return coverage_.data() +
               static_cast<std::size_t>(y - bounds_.y) * static_cast<std::size_t>(bounds_.width);
    }

    // Bounds-checked single read — 0 outside the valid span. For tests and cold callers; the
    // blend loop walks rows directly.
    float coverageAt(int x, int y) const;

   private:
    void addPiece(float ax, float ay, float bx, float by, float halfWidth);
    void extendRow(int y, int x0, int x1);

    Rect bounds_{};
    std::vector<float> coverage_;
    std::vector<int> rowLo_;
    std::vector<int> rowHi_;
};

// The pixel box a polyline of this half-width can touch, intersected with `clip`.
Rect strokeBounds(const StrokePoint* pts, std::size_t count, float halfWidth, const Rect& clip);

// A whole stroke in one pass: bounds, reset, every segment MAX-accumulated. Blending the finished
// canvas exactly once is the caller's half of the contract.
void strokePolyline(StrokeCanvas& canvas, const StrokePoint* pts, std::size_t count,
                    float halfWidth, const Rect& clip);

// Appends a flattened arc (n+1 points for n chords) to `out`. Angles are radians in LICE's
// convention: 0 is 12 o'clock, increasing clockwise — x = cx + r*sin(a), y = cy - r*cos(a).
void appendArc(std::vector<StrokePoint>& out, float cx, float cy, float radius, float startRad,
               float endRad, float flatnessPx = kArcFlatnessPx);

// The row-major element offset of row `y` within a `rowSpan`-elements-per-row pixel buffer,
// accounting for a possibly bottom-up (`flipped`) layout. Pulled out of the shell's LICE blend
// so its flipped branch — dead for every bitmap type the shell actually constructs, and
// otherwise unverifiable without a live LICE surface — is pinned by a host-free test. Matches
// LICE's own row math (`lice.cpp`'s free-function `LICE_GetPixel`: `(h-1-y)*rowspan + x`).
inline std::size_t rasterRowOffset(int y, int height, int rowSpan, bool flipped) {
    const int row = flipped ? height - 1 - y : y;
    return static_cast<std::size_t>(row) * static_cast<std::size_t>(rowSpan);
}

}  // namespace reasampler::ui
