// Copyright (C) 2026 Kiyotsugu Arai
// SPDX-License-Identifier: LGPL-3.0-or-later

/**
 * @file Path2D.hpp
 * @brief 2D パスオブジェクト (7b-7: Cairo/Skia/SVG 相当)
 *
 * moveTo/lineTo/curveTo/arcTo/close でパスを構築し、
 * stroke/fill で描画。
 */

#ifndef CALX_IMAGE_DRAW_PATH2D_HPP
#define CALX_IMAGE_DRAW_PATH2D_HPP

#include <vector>
#include <cmath>
#include <algorithm>

namespace calx::image {

/// パスコマンド
struct PathCmd {
    enum Type { MoveTo, LineTo, QuadTo, CubicTo, Close };
    Type type;
    double x1, y1;  // 制御点1 or 端点
    double x2, y2;  // 制御点2 (CubicTo のみ)
    double x3, y3;  // 端点 (CubicTo のみ)
};

/// 2D パス
class Path2D {
public:
    Path2D& moveTo(double x, double y) {
        cmds_.push_back({PathCmd::MoveTo, x, y}); curX_ = x; curY_ = y;
        if (cmds_.size() == 1) { startX_ = x; startY_ = y; }
        return *this;
    }

    Path2D& lineTo(double x, double y) {
        cmds_.push_back({PathCmd::LineTo, x, y});
        curX_ = x; curY_ = y;
        return *this;
    }

    Path2D& quadTo(double cx, double cy, double x, double y) {
        cmds_.push_back({PathCmd::QuadTo, cx, cy, x, y});
        curX_ = x; curY_ = y;
        return *this;
    }

    Path2D& cubicTo(double c1x, double c1y, double c2x, double c2y,
                     double x, double y) {
        cmds_.push_back({PathCmd::CubicTo, c1x, c1y, c2x, c2y, x, y});
        curX_ = x; curY_ = y;
        return *this;
    }

    Path2D& close() {
        cmds_.push_back({PathCmd::Close, startX_, startY_});
        curX_ = startX_; curY_ = startY_;
        return *this;
    }

    /// 矩形を追加
    Path2D& rect(double x, double y, double w, double h) {
        return moveTo(x,y).lineTo(x+w,y).lineTo(x+w,y+h).lineTo(x,y+h).close();
    }

    /// 円を追加 (4 つの 3 次ベジェで近似)
    Path2D& circle(double cx, double cy, double r) {
        constexpr double k = 0.5522847498;  // 4/3 * (sqrt(2)-1)
        return moveTo(cx+r, cy)
            .cubicTo(cx+r, cy+k*r, cx+k*r, cy+r, cx, cy+r)
            .cubicTo(cx-k*r, cy+r, cx-r, cy+k*r, cx-r, cy)
            .cubicTo(cx-r, cy-k*r, cx-k*r, cy-r, cx, cy-r)
            .cubicTo(cx+k*r, cy-r, cx+r, cy-k*r, cx+r, cy)
            .close();
    }

    /// パスを多角形 (点列) にフラット化
    std::vector<std::pair<double,double>> flatten(int segmentsPerCurve = 8) const {
        std::vector<std::pair<double,double>> pts;
        double px = 0, py = 0;
        for (const auto& cmd : cmds_) {
            switch (cmd.type) {
            case PathCmd::MoveTo:
                pts.push_back({cmd.x1, cmd.y1});
                px = cmd.x1; py = cmd.y1;
                break;
            case PathCmd::LineTo:
                pts.push_back({cmd.x1, cmd.y1});
                px = cmd.x1; py = cmd.y1;
                break;
            case PathCmd::QuadTo:
                for (int i = 1; i <= segmentsPerCurve; ++i) {
                    double t = double(i) / segmentsPerCurve, u = 1-t;
                    pts.push_back({u*u*px + 2*u*t*cmd.x1 + t*t*cmd.x2,
                                   u*u*py + 2*u*t*cmd.y1 + t*t*cmd.y2});
                }
                px = cmd.x2; py = cmd.y2;
                break;
            case PathCmd::CubicTo:
                for (int i = 1; i <= segmentsPerCurve; ++i) {
                    double t = double(i)/segmentsPerCurve, u = 1-t;
                    pts.push_back({u*u*u*px + 3*u*u*t*cmd.x1 + 3*u*t*t*cmd.x2 + t*t*t*cmd.x3,
                                   u*u*u*py + 3*u*u*t*cmd.y1 + 3*u*t*t*cmd.y2 + t*t*t*cmd.y3});
                }
                px = cmd.x3; py = cmd.y3;
                break;
            case PathCmd::Close:
                pts.push_back({cmd.x1, cmd.y1});
                px = cmd.x1; py = cmd.y1;
                break;
            }
        }
        return pts;
    }

    /// Image<T> に stroke 描画
    template<typename T>
    void stroke(Image<T>& img, const std::vector<T>& color, int thickness = 1,
                const LineAAParams& aa = LineAAParams::none()) const {
        auto pts = flatten();
        for (std::size_t i = 0; i + 1 < pts.size(); ++i)
            img.drawLine(int(pts[i].first+.5), int(pts[i].second+.5),
                         int(pts[i+1].first+.5), int(pts[i+1].second+.5),
                         color, thickness, aa);
    }

    /// Image<T> に fill 描画 (偶奇規則)
    template<typename T>
    void fill(Image<T>& img, const std::vector<T>& color) const {
        auto pts = flatten();
        std::vector<std::pair<int,int>> intPts;
        intPts.reserve(pts.size());
        for (auto& [x,y] : pts)
            intPts.push_back({int(x+.5), int(y+.5)});
        img.fillPolygon(intPts, color);
    }

    const std::vector<PathCmd>& commands() const { return cmds_; }
    bool empty() const { return cmds_.empty(); }

    // ================================================================
    // DRW-27: パスユーティリティ
    // ================================================================

    /// パスの総長 (フラット化した折れ線の長さ)
    double length(int segmentsPerCurve = 8) const {
        auto pts = flatten(segmentsPerCurve);
        double len = 0;
        for (std::size_t i = 0; i + 1 < pts.size(); ++i) {
            double dx = pts[i+1].first - pts[i].first;
            double dy = pts[i+1].second - pts[i].second;
            len += std::sqrt(dx*dx + dy*dy);
        }
        return len;
    }

    /// バウンディングボックス {xMin, yMin, xMax, yMax}
    std::array<double, 4> bounds(int segmentsPerCurve = 8) const {
        auto pts = flatten(segmentsPerCurve);
        if (pts.empty()) return {0, 0, 0, 0};
        double xMin = pts[0].first, yMin = pts[0].second;
        double xMax = xMin, yMax = yMin;
        for (auto& [x, y] : pts) {
            xMin = std::min(xMin, x); yMin = std::min(yMin, y);
            xMax = std::max(xMax, x); yMax = std::max(yMax, y);
        }
        return {xMin, yMin, xMax, yMax};
    }

    /// 点がパス内部にあるか (偶奇規則)
    bool contains(double px, double py, int segmentsPerCurve = 8) const {
        auto pts = flatten(segmentsPerCurve);
        if (pts.size() < 3) return false;
        bool inside = false;
        for (std::size_t i = 0, j = pts.size() - 1; i < pts.size(); j = i++) {
            double yi = pts[i].second, yj = pts[j].second;
            double xi = pts[i].first, xj = pts[j].first;
            if ((yi > py) != (yj > py) &&
                px < (xj - xi) * (py - yi) / (yj - yi) + xi)
                inside = !inside;
        }
        return inside;
    }

    /// パスを反転 (巻き方向を逆に)
    Path2D reversed() const {
        auto pts = flatten();
        Path2D rev;
        if (pts.empty()) return rev;
        rev.moveTo(pts.back().first, pts.back().second);
        for (int i = static_cast<int>(pts.size()) - 2; i >= 0; --i)
            rev.lineTo(pts[i].first, pts[i].second);
        rev.close();
        return rev;
    }

    /// パス上の点 (t ∈ [0,1]) を取得
    std::pair<double, double> pointAt(double t, int segmentsPerCurve = 8) const {
        auto pts = flatten(segmentsPerCurve);
        if (pts.empty()) return {0, 0};
        double totalLen = 0;
        std::vector<double> segLens;
        for (std::size_t i = 0; i + 1 < pts.size(); ++i) {
            double dx = pts[i+1].first - pts[i].first;
            double dy = pts[i+1].second - pts[i].second;
            double sl = std::sqrt(dx*dx + dy*dy);
            segLens.push_back(sl);
            totalLen += sl;
        }
        double target = t * totalLen;
        double acc = 0;
        for (std::size_t i = 0; i < segLens.size(); ++i) {
            if (acc + segLens[i] >= target) {
                double f = (segLens[i] > 1e-12) ? (target - acc) / segLens[i] : 0;
                return {pts[i].first + f * (pts[i+1].first - pts[i].first),
                        pts[i].second + f * (pts[i+1].second - pts[i].second)};
            }
            acc += segLens[i];
        }
        return pts.back();
    }

    // ================================================================
    // DRW-28: パスの補間 / モーフィング
    // ================================================================

    /// 2 つのパス間を t ∈ [0,1] で線形補間
    static Path2D interpolate(const Path2D& a, const Path2D& b, double t,
                               int segmentsPerCurve = 8) {
        auto ptsA = a.flatten(segmentsPerCurve);
        auto ptsB = b.flatten(segmentsPerCurve);

        // 点数を揃える (少ない方を線形補間で増やす)
        auto resample = [](const std::vector<std::pair<double,double>>& pts,
                           std::size_t targetN) {
            if (pts.size() == targetN) return pts;
            std::vector<std::pair<double,double>> result;
            result.reserve(targetN);
            for (std::size_t i = 0; i < targetN; ++i) {
                double f = double(i) / (targetN - 1) * (pts.size() - 1);
                int idx = std::min(int(f), int(pts.size()) - 2);
                double frac = f - idx;
                result.push_back({
                    pts[idx].first + frac * (pts[idx+1].first - pts[idx].first),
                    pts[idx].second + frac * (pts[idx+1].second - pts[idx].second)
                });
            }
            return result;
        };

        std::size_t n = std::max(ptsA.size(), ptsB.size());
        ptsA = resample(ptsA, n);
        ptsB = resample(ptsB, n);

        Path2D result;
        if (n == 0) return result;
        double x0 = ptsA[0].first*(1-t) + ptsB[0].first*t;
        double y0 = ptsA[0].second*(1-t) + ptsB[0].second*t;
        result.moveTo(x0, y0);
        for (std::size_t i = 1; i < n; ++i) {
            double x = ptsA[i].first*(1-t) + ptsB[i].first*t;
            double y = ptsA[i].second*(1-t) + ptsB[i].second*t;
            result.lineTo(x, y);
        }
        result.close();
        return result;
    }

    // ================================================================
    // DRW-29: SVG パスデータの読み込み
    // ================================================================

    /// SVG path の d 属性をパース
    /// 対応: M, L, H, V, C, S, Q, T, Z (大文字=絶対, 小文字=相対)
    static Path2D fromSVG(const std::string& d) {
        Path2D path;
        std::size_t i = 0;
        double cx = 0, cy = 0;  // 現在位置
        double lastCx = 0, lastCy = 0;  // 前の制御点 (S, T 用)
        char lastCmd = 0;

        auto skipWS = [&]() {
            while (i < d.size() && (d[i] == ' ' || d[i] == ',' || d[i] == '\n' || d[i] == '\r' || d[i] == '\t')) ++i;
        };
        auto readNum = [&]() -> double {
            skipWS();
            std::size_t start = i;
            if (i < d.size() && (d[i] == '-' || d[i] == '+')) ++i;
            while (i < d.size() && ((d[i] >= '0' && d[i] <= '9') || d[i] == '.')) ++i;
            if (i == start) return 0;
            return std::stod(d.substr(start, i - start));
        };
        auto hasNum = [&]() -> bool {
            skipWS();
            return i < d.size() && (d[i] == '-' || d[i] == '+' || d[i] == '.' || (d[i] >= '0' && d[i] <= '9'));
        };

        while (i < d.size()) {
            skipWS();
            if (i >= d.size()) break;

            char cmd = d[i];
            bool isAlpha = (cmd >= 'A' && cmd <= 'Z') || (cmd >= 'a' && cmd <= 'z');
            if (isAlpha) { ++i; lastCmd = cmd; }
            else cmd = lastCmd;  // 数値が続く場合は前のコマンドを繰り返す

            bool rel = (cmd >= 'a' && cmd <= 'z');
            char C = rel ? (cmd - 32) : cmd;

            if (C == 'M') {
                double x = readNum(), y = readNum();
                if (rel) { x += cx; y += cy; }
                path.moveTo(x, y); cx = x; cy = y;
                lastCmd = rel ? 'l' : 'L';  // 後続は lineTo
            } else if (C == 'L') {
                while (hasNum()) {
                    double x = readNum(), y = readNum();
                    if (rel) { x += cx; y += cy; }
                    path.lineTo(x, y); cx = x; cy = y;
                }
            } else if (C == 'H') {
                while (hasNum()) {
                    double x = readNum();
                    if (rel) x += cx;
                    path.lineTo(x, cy); cx = x;
                }
            } else if (C == 'V') {
                while (hasNum()) {
                    double y = readNum();
                    if (rel) y += cy;
                    path.lineTo(cx, y); cy = y;
                }
            } else if (C == 'C') {
                while (hasNum()) {
                    double x1=readNum(), y1=readNum(), x2=readNum(), y2=readNum(), x=readNum(), y=readNum();
                    if (rel) { x1+=cx; y1+=cy; x2+=cx; y2+=cy; x+=cx; y+=cy; }
                    path.cubicTo(x1, y1, x2, y2, x, y);
                    lastCx = x2; lastCy = y2; cx = x; cy = y;
                }
            } else if (C == 'S') {
                while (hasNum()) {
                    double x2=readNum(), y2=readNum(), x=readNum(), y=readNum();
                    if (rel) { x2+=cx; y2+=cy; x+=cx; y+=cy; }
                    double x1 = 2*cx - lastCx, y1 = 2*cy - lastCy;
                    path.cubicTo(x1, y1, x2, y2, x, y);
                    lastCx = x2; lastCy = y2; cx = x; cy = y;
                }
            } else if (C == 'Q') {
                while (hasNum()) {
                    double x1=readNum(), y1=readNum(), x=readNum(), y=readNum();
                    if (rel) { x1+=cx; y1+=cy; x+=cx; y+=cy; }
                    path.quadTo(x1, y1, x, y);
                    lastCx = x1; lastCy = y1; cx = x; cy = y;
                }
            } else if (C == 'T') {
                while (hasNum()) {
                    double x=readNum(), y=readNum();
                    if (rel) { x+=cx; y+=cy; }
                    double x1 = 2*cx - lastCx, y1 = 2*cy - lastCy;
                    path.quadTo(x1, y1, x, y);
                    lastCx = x1; lastCy = y1; cx = x; cy = y;
                }
            } else if (C == 'Z') {
                path.close();
                cx = path.startX_; cy = path.startY_;
            }
        }
        return path;
    }

private:
    std::vector<PathCmd> cmds_;
    double curX_ = 0, curY_ = 0;
    double startX_ = 0, startY_ = 0;
};

// ================================================================
// DRW-26: パスのブーリアン演算 (ポリゴンベース簡易版)
// ================================================================

enum class PathOp { Union, Intersection, Difference, Xor };

/// ブーリアン演算 (フラット化 → ポリゴンクリッピング)
/// Sutherland-Hodgman アルゴリズムの拡張
inline Path2D pathBooleanOp(const Path2D& a, const Path2D& b, PathOp op,
                             int segments = 8) {
    auto ptsA = a.flatten(segments);
    auto ptsB = b.flatten(segments);

    // 簡易版: 点の内外判定で分類し、結果パスを構築
    auto pointInPoly = [](double px, double py,
                          const std::vector<std::pair<double,double>>& poly) -> bool {
        bool inside = false;
        for (std::size_t i = 0, j = poly.size() - 1; i < poly.size(); j = i++) {
            double yi = poly[i].second, yj = poly[j].second;
            double xi = poly[i].first, xj = poly[j].first;
            if ((yi > py) != (yj > py) &&
                px < (xj - xi) * (py - yi) / (yj - yi) + xi)
                inside = !inside;
        }
        return inside;
    };

    // 結果の点を収集
    std::vector<std::pair<double,double>> result;

    auto shouldInclude = [&](double px, double py, bool inA, bool inB) -> bool {
        switch (op) {
        case PathOp::Union:        return inA || inB;
        case PathOp::Intersection: return inA && inB;
        case PathOp::Difference:   return inA && !inB;
        case PathOp::Xor:          return inA != inB;
        }
        return false;
    };

    // A の点で B の内外を判定
    for (auto& [x, y] : ptsA) {
        bool inB = pointInPoly(x, y, ptsB);
        if (shouldInclude(x, y, true, inB))
            result.push_back({x, y});
    }

    // B の点で A の内外を判定
    for (auto& [x, y] : ptsB) {
        bool inA = pointInPoly(x, y, ptsA);
        if (shouldInclude(x, y, inA, true))
            result.push_back({x, y});
    }

    // 凸包で結果パスを構築 (簡易: 点順序は未ソート)
    Path2D out;
    if (result.empty()) return out;

    // 重心からの角度でソート
    double cx = 0, cy = 0;
    for (auto& [x, y] : result) { cx += x; cy += y; }
    cx /= result.size(); cy /= result.size();
    std::sort(result.begin(), result.end(), [cx, cy](auto& a, auto& b) {
        return std::atan2(a.second - cy, a.first - cx) < std::atan2(b.second - cy, b.first - cx);
    });

    out.moveTo(result[0].first, result[0].second);
    for (std::size_t i = 1; i < result.size(); ++i)
        out.lineTo(result[i].first, result[i].second);
    out.close();
    return out;
}

} // namespace calx::image

#endif
