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

/**
 * @file convenience.hpp
 * @brief OpenCV で書くのが大変な処理を 1 行で実現する便利関数群
 *
 * CVX-1: drawLabel       — ラベル付き矩形 (検出結果の可視化)
 * CVX-2: (alpha は Image.hpp の blendPixel で対応済み)
 * CVX-3: overlay          — RGBA 画像の貼り付け (アルファ合成)
 * CVX-4: safeCrop         — 安全なクロップ (範囲外自動クリップ)
 * CVX-5: paste            — 安全なペースト (境界自動クリップ)
 * CVX-6: resizeKeepAspect — アスペクト比保持リサイズ (letterbox)
 * CVX-7: visualDiff       — 画像比較/差分可視化
 * CVX-8: tileImages       — 画像グリッド表示
 * CVX-9: rotateFit        — はみ出さない回転
 */

#ifndef CALX_IMAGE_CONVENIENCE_HPP
#define CALX_IMAGE_CONVENIENCE_HPP

#include <image/Image.hpp>
#include <image/Image_impl.hpp>  // asFloat/asUint8 は uint8_t 版が必要
#include <image/io/bmp.hpp>
#include <string>
#include <vector>
#include <cmath>
#include <algorithm>
#include <functional>

namespace calx::image {

// ========================================================================
// CVX-1: drawLabel — ラベル付き矩形
// ========================================================================

/// ラベル描画オプション
template<typename T>
struct LabelStyle {
    std::vector<T> bg;          // 背景色 (空=自動: boxColor の暗い版)
    std::vector<T> fg;          // 文字色 (空=自動: 白 or 黒)
    int padding = 2;            // テキスト周囲のパディング (px)
    int fontScale = 1;          // フォントスケール
    T alpha = T(1);             // 全体の不透明度
};

/// 矩形 + ラベルを描画 (検出結果の可視化用)
template<typename T>
void drawLabel(Image<T>& img, const std::string& text,
               int x0, int y0, int x1, int y1,
               const std::vector<T>& boxColor,
               const LabelStyle<T>& style = {},
               const LineAAParams& aa = LineAAParams::none()) {
    // 矩形を描画
    img.drawRect(x0, y0, x1, y1, boxColor, 2, aa);

    // テキスト背景色
    std::vector<T> bg = style.bg;
    if (bg.empty()) bg = boxColor;

    // テキスト色 (明るい背景には黒、暗い背景には白)
    std::vector<T> fg = style.fg;
    if (fg.empty()) {
        T brightness = T(0);
        for (auto v : bg) brightness += v;
        brightness /= T(bg.size());
        fg = (brightness > T(0.5))
            ? std::vector<T>{T(0), T(0), T(0)}
            : std::vector<T>{T(1), T(1), T(1)};
    }

    int s = style.fontScale;
    int textH = 7 * s;
    int textW = static_cast<int>(text.size()) * 6 * s;
    int pad = style.padding;

    // ラベル背景矩形
    int lx0 = x0, ly0 = y0 - textH - pad * 2;
    int lx1 = x0 + textW + pad * 2, ly1 = y0;
    if (ly0 < 0) { ly0 = y1; ly1 = y1 + textH + pad * 2; }  // 上にスペースがなければ下に

    // 背景塗りつぶし (半透明対応)
    for (int y = std::max(0, ly0); y < std::min(static_cast<int>(img.height()), ly1); ++y)
        for (int x = std::max(0, lx0); x < std::min(static_cast<int>(img.width()), lx1); ++x)
            img.blendPixel(x, y, bg, style.alpha);

    // テキスト描画
    img.drawText(lx0 + pad, ly0 + pad, text, fg, s);
}

// ========================================================================
// CVX-3: overlay — RGBA 画像の貼り付け
// ========================================================================

/// RGBA (4ch) 前景画像を背景画像にアルファ合成で貼り付け
template<typename T>
void overlay(Image<T>& bg, const Image<T>& fg, int x, int y) {
    bool hasAlpha = (fg.numChannels() >= 4);
    int bgCh = std::min(bg.numChannels(), 3);

    for (int fy = 0; fy < static_cast<int>(fg.height()); ++fy) {
        int by = y + fy;
        if (by < 0 || by >= static_cast<int>(bg.height())) continue;
        for (int fx = 0; fx < static_cast<int>(fg.width()); ++fx) {
            int bx = x + fx;
            if (bx < 0 || bx >= static_cast<int>(bg.width())) continue;

            T alpha = hasAlpha ? fg.at(fy, fx, 3) : T(1);
            if (alpha <= T(0)) continue;
            T inv = T(1) - std::min(alpha, T(1));

            for (int c = 0; c < bgCh && c < fg.numChannels(); ++c)
                bg.at(by, bx, c) = bg.at(by, bx, c) * inv + fg.at(fy, fx, c) * alpha;
        }
    }
}

// ========================================================================
// CVX-4: safeCrop — 安全なクロップ
// ========================================================================

/// 範囲外を自動クリップしてクロップ (クラッシュしない)
template<typename T>
Image<T> safeCrop(const Image<T>& img, int x, int y, int w, int h) {
    // クリップ
    int sx = std::max(0, x), sy = std::max(0, y);
    int ex = std::min(static_cast<int>(img.width()), x + w);
    int ey = std::min(static_cast<int>(img.height()), y + h);
    if (sx >= ex || sy >= ey) return Image<T>(0, 0, img.numChannels());

    // オフセット (元の矩形内での位置)
    int ox = sx - x, oy = sy - y;
    int cw = ex - sx, ch = ey - sy;

    Image<T> result(h, w, img.numChannels(), T(0));
    for (int cy = 0; cy < ch; ++cy)
        for (int cx = 0; cx < cw; ++cx)
            for (int c = 0; c < img.numChannels(); ++c)
                result.at(oy + cy, ox + cx, c) = img.at(sy + cy, sx + cx, c);
    return result;
}

// ========================================================================
// CVX-5: paste — 安全なペースト
// ========================================================================

/// 境界自動クリップ付き画像貼り付け
template<typename T>
void paste(Image<T>& dst, const Image<T>& src, int x, int y) {
    int ch = std::min(dst.numChannels(), src.numChannels());
    for (int sy = 0; sy < static_cast<int>(src.height()); ++sy) {
        int dy = y + sy;
        if (dy < 0 || dy >= static_cast<int>(dst.height())) continue;
        for (int sx = 0; sx < static_cast<int>(src.width()); ++sx) {
            int dx = x + sx;
            if (dx < 0 || dx >= static_cast<int>(dst.width())) continue;
            for (int c = 0; c < ch; ++c)
                dst.at(dy, dx, c) = src.at(sy, sx, c);
        }
    }
}

// ========================================================================
// CVX-6: resizeKeepAspect — アスペクト比保持リサイズ
// ========================================================================

/// アスペクト比を維持してリサイズ (余白をパディング)
template<typename T>
Image<T> resizeKeepAspect(const Image<T>& img, std::size_t targetW, std::size_t targetH,
                           T padValue = T(0)) {
    double scaleX = double(targetW) / img.width();
    double scaleY = double(targetH) / img.height();
    double scale = std::min(scaleX, scaleY);

    std::size_t newW = static_cast<std::size_t>(img.width() * scale + 0.5);
    std::size_t newH = static_cast<std::size_t>(img.height() * scale + 0.5);
    auto resized = img.resize(newH, newW);

    Image<T> result(targetH, targetW, img.numChannels(), padValue);
    int ox = static_cast<int>((targetW - newW) / 2);
    int oy = static_cast<int>((targetH - newH) / 2);
    paste(result, resized, ox, oy);
    return result;
}

// ========================================================================
// CVX-7: visualDiff — 画像比較/差分可視化
// ========================================================================

/// 2 画像の差異をハイライト表示
template<typename T>
Image<T> visualDiff(const Image<T>& img1, const Image<T>& img2,
                     const std::vector<T>& highlightColor = {T(1), T(0), T(0)},
                     T threshold = T(0.1)) {
    std::size_t h = std::min(img1.height(), img2.height());
    std::size_t w = std::min(img1.width(), img2.width());
    int ch = std::min(img1.numChannels(), img2.numChannels());
    Image<T> result(h, w, 3, T(0));

    for (std::size_t y = 0; y < h; ++y)
        for (std::size_t x = 0; x < w; ++x) {
            T maxDiff = T(0);
            for (int c = 0; c < ch; ++c)
                maxDiff = std::max(maxDiff, std::abs(img1.at(y, x, c) - img2.at(y, x, c)));

            if (maxDiff > threshold) {
                for (int c = 0; c < 3 && c < static_cast<int>(highlightColor.size()); ++c)
                    result.at(y, x, c) = highlightColor[c];
            } else {
                T gray = T(0);
                for (int c = 0; c < ch; ++c) gray += img1.at(y, x, c);
                gray /= T(ch);
                result.at(y, x, 0) = gray * T(0.5);
                result.at(y, x, 1) = gray * T(0.5);
                result.at(y, x, 2) = gray * T(0.5);
            }
        }
    return result;
}

// ========================================================================
// CVX-8: tileImages — 画像グリッド
// ========================================================================

/// 複数画像をグリッド配置
template<typename T>
Image<T> tileImages(const std::vector<Image<T>>& images, int cols,
                     std::size_t cellW = 0, std::size_t cellH = 0,
                     T padValue = T(0)) {
    if (images.empty()) return {};
    if (cols <= 0) cols = static_cast<int>(images.size());
    int rows = (static_cast<int>(images.size()) + cols - 1) / cols;

    // セルサイズ自動決定
    if (cellW == 0 || cellH == 0) {
        for (const auto& img : images) {
            cellW = std::max(cellW, img.width());
            cellH = std::max(cellH, img.height());
        }
    }

    int ch = images[0].numChannels();
    Image<T> result(cellH * rows, cellW * cols, ch, padValue);

    for (std::size_t i = 0; i < images.size(); ++i) {
        int r = static_cast<int>(i) / cols;
        int c = static_cast<int>(i) % cols;
        auto resized = images[i].resize(cellH, cellW);
        paste(result, resized, static_cast<int>(c * cellW), static_cast<int>(r * cellH));
    }
    return result;
}

// ========================================================================
// CVX-9: rotateFit — はみ出さない回転
// ========================================================================

/// 任意角度回転 (キャンバス自動拡大、内容が切れない)
template<typename T>
Image<T> rotateFit(const Image<T>& img, double angleDeg, T padValue = T(0)) {
    constexpr double pi = 3.14159265358979323846;
    double rad = angleDeg * pi / 180.0;
    double cosA = std::abs(std::cos(rad)), sinA = std::abs(std::sin(rad));

    std::size_t newW = static_cast<std::size_t>(img.width() * cosA + img.height() * sinA + 0.5);
    std::size_t newH = static_cast<std::size_t>(img.width() * sinA + img.height() * cosA + 0.5);

    Image<T> result(newH, newW, img.numChannels(), padValue);
    double cx0 = img.width() * 0.5, cy0 = img.height() * 0.5;
    double cx1 = newW * 0.5, cy1 = newH * 0.5;
    double cosR = std::cos(-rad), sinR = std::sin(-rad);

    for (std::size_t y = 0; y < newH; ++y)
        for (std::size_t x = 0; x < newW; ++x) {
            double dx = x - cx1, dy = y - cy1;
            double sx = cosR * dx - sinR * dy + cx0;
            double sy = sinR * dx + cosR * dy + cy0;
            int ix = static_cast<int>(sx), iy = static_cast<int>(sy);
            if (ix >= 0 && ix < static_cast<int>(img.width()) &&
                iy >= 0 && iy < static_cast<int>(img.height()))
                for (int c = 0; c < img.numChannels(); ++c)
                    result.at(y, x, c) = img.at(iy, ix, c);
        }
    return result;
}

// ========================================================================
// CVX-11: asFloat / asUint8 — 型変換
// ========================================================================

/// 画像を float [0,1] に変換 (uint8: /255, double: キャスト)
template<typename T>
Image<float> asFloat(const Image<T>& img) {
    Image<float> result(img.height(), img.width(), img.numChannels());
    for (int c = 0; c < img.numChannels(); ++c)
        for (std::size_t y = 0; y < img.height(); ++y)
            for (std::size_t x = 0; x < img.width(); ++x) {
                if constexpr (std::is_same_v<T, uint8_t>)
                    result.at(y, x, c) = float(img.at(y, x, c)) / 255.0f;
                else
                    result.at(y, x, c) = static_cast<float>(img.at(y, x, c));
            }
    return result;
}

/// 画像を uint8 [0,255] に変換 (float/double: *255+clamp)
template<typename T>
Image<uint8_t> asUint8(const Image<T>& img) {
    Image<uint8_t> result(img.height(), img.width(), img.numChannels());
    for (int c = 0; c < img.numChannels(); ++c)
        for (std::size_t y = 0; y < img.height(); ++y)
            for (std::size_t x = 0; x < img.width(); ++x) {
                if constexpr (std::is_same_v<T, uint8_t>)
                    result.at(y, x, c) = img.at(y, x, c);
                else {
                    double v = static_cast<double>(img.at(y, x, c)) * 255.0 + 0.5;
                    result.at(y, x, c) = static_cast<uint8_t>(std::clamp(v, 0.0, 255.0));
                }
            }
    return result;
}

// ========================================================================
// CVX-12: toHSV360 — HSV 0-360 統一変換
// ========================================================================

/// RGB → HSV (H=0-360, S=0-1, V=0-1, float)
/// 8bit の H=0-180 問題を解消。常に float で返す。
template<typename T>
Image<float> toHSV360(const Image<T>& img) {
    assert(img.numChannels() >= 3);
    Image<float> result(img.height(), img.width(), 3);
    for (std::size_t y = 0; y < img.height(); ++y)
        for (std::size_t x = 0; x < img.width(); ++x) {
            float r, g, b;
            if constexpr (std::is_same_v<T, uint8_t>) {
                r = img.at(y, x, 0) / 255.0f;
                g = img.at(y, x, 1) / 255.0f;
                b = img.at(y, x, 2) / 255.0f;
            } else {
                r = static_cast<float>(img.at(y, x, 0));
                g = static_cast<float>(img.at(y, x, 1));
                b = static_cast<float>(img.at(y, x, 2));
            }
            float mx = std::max({r, g, b}), mn = std::min({r, g, b});
            float d = mx - mn;
            float h = 0, s = (mx > 1e-6f) ? d / mx : 0.0f, v = mx;
            if (d > 1e-6f) {
                if (mx == r)      h = 60.0f * std::fmod((g - b) / d + 6.0f, 6.0f);
                else if (mx == g) h = 60.0f * ((b - r) / d + 2.0f);
                else              h = 60.0f * ((r - g) / d + 4.0f);
            }
            result.at(y, x, 0) = h;  // 0-360
            result.at(y, x, 1) = s;  // 0-1
            result.at(y, x, 2) = v;  // 0-1
        }
    return result;
}

/// HSV (H=0-360, S=0-1, V=0-1) → RGB float [0,1]
template<typename T = float>
Image<T> fromHSV360(const Image<float>& hsv) {
    assert(hsv.numChannels() >= 3);
    Image<T> result(hsv.height(), hsv.width(), 3);
    for (std::size_t y = 0; y < hsv.height(); ++y)
        for (std::size_t x = 0; x < hsv.width(); ++x) {
            float h = hsv.at(y, x, 0), s = hsv.at(y, x, 1), v = hsv.at(y, x, 2);
            float c = v * s, hp = h / 60.0f, x2 = c * (1.0f - std::abs(std::fmod(hp, 2.0f) - 1.0f));
            float m = v - c;
            float r, g, b;
            if      (hp < 1) { r = c; g = x2; b = 0; }
            else if (hp < 2) { r = x2; g = c; b = 0; }
            else if (hp < 3) { r = 0; g = c; b = x2; }
            else if (hp < 4) { r = 0; g = x2; b = c; }
            else if (hp < 5) { r = x2; g = 0; b = c; }
            else             { r = c; g = 0; b = x2; }
            result.at(y, x, 0) = T(r + m);
            result.at(y, x, 1) = T(g + m);
            result.at(y, x, 2) = T(b + m);
        }
    return result;
}

// ========================================================================
// CVX-13: pipe — 処理パイプライン (fluent API)
// ========================================================================

/// 画像パイプラインビルダー
template<typename T>
class ImagePipe {
public:
    explicit ImagePipe(Image<T> img) : img_(std::move(img)) {}

    /// 関数適用 (Image → Image)
    ImagePipe& apply(std::function<Image<T>(const Image<T>&)> fn) {
        img_ = fn(img_); return *this;
    }

    /// operator>> で関数をチェーン
    ImagePipe& operator>>(std::function<Image<T>(const Image<T>&)> fn) {
        return apply(fn);
    }

    /// 結果を取り出す
    Image<T>& get() { return img_; }
    const Image<T>& get() const { return img_; }
    operator Image<T>() && { return std::move(img_); }

private:
    Image<T> img_;
};

/// パイプラインを開始
template<typename T>
ImagePipe<T> pipe(const Image<T>& img) { return ImagePipe<T>(img); }
template<typename T>
ImagePipe<T> pipe(Image<T>&& img) { return ImagePipe<T>(std::move(img)); }

// ========================================================================
// CVX-14: batchProcess — バッチ処理
// ========================================================================
// std::filesystem 依存のため、C++17 が必要

#if __has_include(<filesystem>)
#include <filesystem>

/// フォルダ内の全画像に関数を適用して保存
template<typename T, typename Func>
int batchProcess(const std::string& inputPattern, const std::string& outputDir,
                  Func&& func) {
    namespace fs = std::filesystem;
    fs::path inDir = fs::path(inputPattern).parent_path();
    std::string ext = fs::path(inputPattern).extension().string();
    if (!fs::exists(inDir)) return 0;
    fs::create_directories(outputDir);

    int count = 0;
    for (const auto& entry : fs::directory_iterator(inDir)) {
        if (!entry.is_regular_file()) continue;
        auto p = entry.path();
        if (!ext.empty() && p.extension() != ext) continue;

        // 画像読み込み (BMP のみ — 他の形式は別途)
        try {
            auto img = loadBmp<T>(p.string());
            func(img);
            auto outPath = fs::path(outputDir) / p.filename();
            saveBmp(outPath.string(), img);
            ++count;
        } catch (...) {
            // 読み込みエラーはスキップ
        }
    }
    return count;
}

#endif // __has_include(<filesystem>)

} // namespace calx::image

#endif // CALX_IMAGE_CONVENIENCE_HPP
