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

// video.hpp — 動画 I/O
//
// 新規実装:
//   - 連番 BMP の読み書き
//   - 非圧縮 AVI 出力

#ifndef CALX_IMAGE_IO_VIDEO_HPP
#define CALX_IMAGE_IO_VIDEO_HPP

#include <image/Image.hpp>
#include <image/io/bmp.hpp>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>

namespace calx::image {

    /// 連番 BMP としてフレームを保存
    /// @param basePath  ベースパス (例: "output/frame_")
    /// @param frames    フレームのリスト
    template<typename T>
    void saveFramesBmp(const std::string& basePath, const std::vector<Image<T>>& frames) {
        for (std::size_t i = 0; i < frames.size(); ++i) {
            char buf[32];
            std::snprintf(buf, sizeof(buf), "%05zu", i);
            std::string path = basePath + buf + ".bmp";
            saveBmp(path, frames[i]);
        }
    }

    /// 非圧縮 AVI ファイルライター
    class AviWriter {
    public:
        /// @param path    出力ファイルパス
        /// @param width   フレーム幅
        /// @param height  フレーム高さ
        /// @param fps     フレームレート
        AviWriter(const std::string& path, int width, int height, int fps = 30)
            : width_(width), height_(height), fps_(fps), frameCount_(0) {
            ofs_.open(path, std::ios::binary);
            if (!ofs_) return;

            // AVI ヘッダーは後で書き戻す
            headerPos_ = ofs_.tellp();
            writeAviHeader();
        }

        /// フレームを追加
        template<typename T>
        void addFrame(const Image<T>& frame) {
            if (!ofs_) return;

            int rowBytes = ((width_ * 3 + 3) / 4) * 4;
            int frameSize = rowBytes * height_;

            // "00dc" チャンク (非圧縮 DIB)
            writeChunkHeader("00dc", frameSize);

            std::vector<uint8_t> row(rowBytes, 0);
            bool isColor = frame.numChannels() >= 3;

            for (int y = height_ - 1; y >= 0; --y) {
                for (int x = 0; x < width_; ++x) {
                    if (static_cast<int>(y) < static_cast<int>(frame.height()) &&
                        static_cast<int>(x) < static_cast<int>(frame.width())) {
                        if (isColor) {
                            row[x*3+0] = detail::toU8(frame.at(y, x, 2));
                            row[x*3+1] = detail::toU8(frame.at(y, x, 1));
                            row[x*3+2] = detail::toU8(frame.at(y, x, 0));
                        } else {
                            uint8_t v = detail::toU8(frame.at(y, x));
                            row[x*3+0] = row[x*3+1] = row[x*3+2] = v;
                        }
                    }
                }
                ofs_.write(reinterpret_cast<const char*>(row.data()), rowBytes);
            }

            ++frameCount_;
            moviSize_ += 8 + frameSize;
        }

        /// ファイルを閉じる (ヘッダーを更新)
        void close() {
            if (!ofs_) return;

            // movi リストの終了
            auto endPos = ofs_.tellp();

            // ヘッダーを書き直し
            ofs_.seekp(headerPos_);
            writeAviHeader();

            ofs_.close();
        }

        ~AviWriter() { if (ofs_.is_open()) close(); }

    private:
        std::ofstream ofs_;
        int width_, height_, fps_;
        int frameCount_ = 0;
        std::streampos headerPos_;
        uint32_t moviSize_ = 4;  // "movi" の 4 バイト

        void write32(uint32_t v) { ofs_.write(reinterpret_cast<const char*>(&v), 4); }
        void write16(uint16_t v) { ofs_.write(reinterpret_cast<const char*>(&v), 2); }
        void writeFourCC(const char* cc) { ofs_.write(cc, 4); }

        void writeChunkHeader(const char* fourcc, uint32_t size) {
            writeFourCC(fourcc);
            write32(size);
        }

        void writeAviHeader() {
            int rowBytes = ((width_ * 3 + 3) / 4) * 4;
            uint32_t frameSize = rowBytes * height_;
            uint32_t usPerFrame = (fps_ > 0) ? (1000000 / fps_) : 33333;

            // hdrl サイズ: avih(56+8) + strl(strf+strh+LIST header)
            uint32_t avihSize = 56;
            uint32_t strhSize = 56;
            uint32_t strfSize = 40;
            uint32_t strlSize = 4 + 8 + strhSize + 8 + strfSize;
            uint32_t hdrlSize = 4 + 8 + avihSize + 8 + strlSize;
            uint32_t totalSize = 4 + 8 + hdrlSize + 8 + moviSize_;

            // RIFF header
            writeFourCC("RIFF");
            write32(totalSize);
            writeFourCC("AVI ");

            // LIST hdrl
            writeFourCC("LIST");
            write32(hdrlSize);
            writeFourCC("hdrl");

            // avih
            writeChunkHeader("avih", avihSize);
            write32(usPerFrame);          // dwMicroSecPerFrame
            write32(frameSize * fps_);    // dwMaxBytesPerSec
            write32(0);                   // dwPaddingGranularity
            write32(0x10);                // dwFlags (AVIF_HASINDEX)
            write32(frameCount_);         // dwTotalFrames
            write32(0);                   // dwInitialFrames
            write32(1);                   // dwStreams
            write32(frameSize);           // dwSuggestedBufferSize
            write32(width_);              // dwWidth
            write32(height_);             // dwHeight
            write32(0); write32(0); write32(0); write32(0);  // dwReserved[4]

            // LIST strl
            writeFourCC("LIST");
            write32(strlSize);
            writeFourCC("strl");

            // strh
            writeChunkHeader("strh", strhSize);
            writeFourCC("vids");          // fccType
            write32(0);                   // fccHandler (DIB = 0)
            write32(0);                   // dwFlags
            write16(0);                   // wPriority
            write16(0);                   // wLanguage
            write32(0);                   // dwInitialFrames
            write32(1);                   // dwScale
            write32(fps_);                // dwRate
            write32(0);                   // dwStart
            write32(frameCount_);         // dwLength
            write32(frameSize);           // dwSuggestedBufferSize
            write32(0xFFFFFFFF);          // dwQuality
            write32(0);                   // dwSampleSize
            write16(0); write16(0);       // rcFrame (left, top)
            write16(static_cast<uint16_t>(width_));
            write16(static_cast<uint16_t>(height_));

            // strf (BITMAPINFOHEADER)
            writeChunkHeader("strf", strfSize);
            write32(40);                  // biSize
            write32(width_);              // biWidth
            write32(height_);             // biHeight
            write16(1);                   // biPlanes
            write16(24);                  // biBitCount
            write32(0);                   // biCompression (BI_RGB)
            write32(frameSize);           // biSizeImage
            write32(0); write32(0);       // biXPelsPerMeter, biYPelsPerMeter
            write32(0); write32(0);       // biClrUsed, biClrImportant

            // LIST movi
            writeFourCC("LIST");
            write32(moviSize_);
            writeFourCC("movi");
        }
    };

// ========================================================================
// AVI 読み込み (非圧縮 DIB)
// ========================================================================

template<typename T>
class AviReader {
public:
    explicit AviReader(const std::string& path) {
        ifs_.open(path, std::ios::binary);
        if (!ifs_) throw std::runtime_error("AviReader: cannot open " + path);
        parseHeader();
    }

    int width() const { return width_; }
    int height() const { return height_; }
    double fps() const { return fps_; }
    uint32_t frameCount() const { return frameCount_; }

    /// 次のフレームを読み込み (EOF で空 Image)
    Image<T> readFrame() {
        if (currentFrame_ >= frameCount_) return {};
        char id[4]; uint32_t sz;
        while (ifs_.read(id, 4) && ifs_.read(reinterpret_cast<char*>(&sz), 4)) {
            if (std::memcmp(id, "00db", 4) == 0 || std::memcmp(id, "00dc", 4) == 0)
                return decodeDIB(sz);
            ifs_.seekg(sz + (sz & 1), std::ios::cur);
        }
        return {};
    }

    /// 全フレーム読み込み
    std::vector<Image<T>> readAll() {
        std::vector<Image<T>> frames;
        while (currentFrame_ < frameCount_) {
            auto f = readFrame();
            if (f.empty()) break;
            frames.push_back(std::move(f));
        }
        return frames;
    }

private:
    std::ifstream ifs_;
    int width_ = 0, height_ = 0;
    double fps_ = 30;
    uint32_t frameCount_ = 0, currentFrame_ = 0;

    void parseHeader() {
        char buf[12];
        ifs_.read(buf, 12);
        if (std::memcmp(buf, "RIFF", 4) != 0 || std::memcmp(buf + 8, "AVI ", 4) != 0)
            throw std::runtime_error("AviReader: not an AVI file");
        while (ifs_) {
            char id[4]; uint32_t sz;
            if (!ifs_.read(id, 4) || !ifs_.read(reinterpret_cast<char*>(&sz), 4)) break;
            if (std::memcmp(id, "avih", 4) == 0) {
                char avih[56]; ifs_.read(avih, std::min(sz, uint32_t(56)));
                uint32_t usPerFrame; std::memcpy(&usPerFrame, avih, 4);
                fps_ = (usPerFrame > 0) ? 1e6 / usPerFrame : 30.0;
                std::memcpy(&frameCount_, avih + 16, 4);
                std::memcpy(&width_, avih + 32, 4);
                std::memcpy(&height_, avih + 36, 4);
                if (sz > 56) ifs_.seekg(sz - 56, std::ios::cur);
            } else if (std::memcmp(id, "strf", 4) == 0) {
                if (sz >= 40) {
                    char bih[40]; ifs_.read(bih, 40);
                    int32_t w, h; std::memcpy(&w, bih + 4, 4); std::memcpy(&h, bih + 8, 4);
                    width_ = w; height_ = std::abs(h);
                    if (sz > 40) ifs_.seekg(sz - 40, std::ios::cur);
                } else ifs_.seekg(sz, std::ios::cur);
            } else if (std::memcmp(id, "LIST", 4) == 0) {
                char lt[4]; ifs_.read(lt, 4);
                if (std::memcmp(lt, "movi", 4) == 0) break;
            } else {
                ifs_.seekg(sz + (sz & 1), std::ios::cur);
            }
        }
    }

    Image<T> decodeDIB(uint32_t sz) {
        int rowBytes = ((width_ * 3 + 3) / 4) * 4;
        Image<T> frame(height_, width_, 3, T(0));
        std::vector<uint8_t> row(rowBytes);
        for (int y = height_ - 1; y >= 0; --y) {
            ifs_.read(reinterpret_cast<char*>(row.data()), rowBytes);
            for (int x = 0; x < width_; ++x) {
                if constexpr (std::is_same_v<T, uint8_t>) {
                    frame.at(y, x, 0) = row[x*3+2];
                    frame.at(y, x, 1) = row[x*3+1];
                    frame.at(y, x, 2) = row[x*3+0];
                } else {
                    frame.at(y, x, 0) = T(row[x*3+2]) / T(255);
                    frame.at(y, x, 1) = T(row[x*3+1]) / T(255);
                    frame.at(y, x, 2) = T(row[x*3+0]) / T(255);
                }
            }
        }
        ++currentFrame_;
        return frame;
    }
};

// ========================================================================
// 連番 BMP 読み込み
// ========================================================================

/// 連番 BMP を読み込み (frame_0000.bmp, frame_0001.bmp, ...)
template<typename T>
std::vector<Image<T>> loadFramesBmp(const std::string& dir,
                                      const std::string& prefix = "frame_") {
    std::vector<Image<T>> frames;
    for (int i = 0; ; ++i) {
        char buf[64];
        std::snprintf(buf, sizeof(buf), "%s%04d.bmp", prefix.c_str(), i);
        auto path = std::filesystem::path(dir) / buf;
        if (!std::filesystem::exists(path)) break;
        frames.push_back(loadBmp<T>(path.string()));
    }
    return frames;
}

// ========================================================================
// 動画処理パイプライン (CVX-15)
// ========================================================================

/// AVI の読み → フレーム処理 → 書き出しを 1 行で
template<typename T>
void processVideo(const std::string& inputPath, const std::string& outputPath,
                   std::function<void(Image<T>&)> processFrame) {
    AviReader<T> reader(inputPath);
    AviWriter<T> writer(outputPath, reader.width(), reader.height(), reader.fps());
    for (uint32_t i = 0; i < reader.frameCount(); ++i) {
        auto frame = reader.readFrame();
        if (frame.empty()) break;
        processFrame(frame);
        writer.addFrame(frame);
    }
    writer.close();
}

// ========================================================================
// MP4/動画 読み書き (ffmpeg CLI ラッパー)
// ========================================================================
//
// ffmpeg/ffprobe コマンドをパイプ経由で呼び出し、任意の動画形式の読み書きを提供。
// H.264/H.265, VP9, AV1 等の全コーデック、字幕 (SRT/ASS) にも対応。
// ffmpeg がシステム PATH に存在する必要がある。
//
// Usage:
//   // 読み込み
//   VideoReader<float> reader("input.mp4");
//   while (auto frame = reader.readFrame()) { process(*frame); }
//
//   // 書き込み (H.264, CRF 23)
//   VideoWriterOptions opt; opt.codec = "libx264"; opt.crf = 23;
//   VideoWriter writer("output.mp4", 1920, 1080, 30, opt);
//   writer.addFrame(image);
//   writer.close();
//
//   // 字幕付き書き出し
//   opt.subtitleFile = "sub.srt";
//   VideoWriter writer("output.mp4", 1920, 1080, 30, opt);

#include <array>
#include <memory>
#include <optional>
#include <stdexcept>
#include <cstdio>

namespace detail {
#ifdef _WIN32
    inline FILE* popen_wrap(const char* cmd, const char* mode) { return _popen(cmd, mode); }
    inline int pclose_wrap(FILE* f) { return _pclose(f); }
#else
    inline FILE* popen_wrap(const char* cmd, const char* mode) { return popen(cmd, mode); }
    inline int pclose_wrap(FILE* f) { return pclose(f); }
#endif
} // namespace detail

/// ffmpeg CLI が利用可能か
inline bool isFFmpegAvailable() {
#ifdef _WIN32
    return std::system("where ffmpeg >nul 2>nul") == 0;
#else
    return std::system("which ffmpeg >/dev/null 2>&1") == 0;
#endif
}

/// 動画の情報を ffprobe で取得
struct VideoInfo {
    int width = 0, height = 0;
    double fps = 0;
    double duration = 0;     // 秒
    int64_t frameCount = 0;
    std::string codec;

    static VideoInfo probe(const std::string& path) {
        VideoInfo info;
        std::string cmd = "ffprobe -v error -select_streams v:0"
            " -show_entries stream=width,height,r_frame_rate,codec_name,nb_frames"
            " -show_entries format=duration"
            " -of csv=p=0:s=, \"" + path + "\"";
#ifdef _WIN32
        cmd += " 2>nul";
#else
        cmd += " 2>/dev/null";
#endif
        std::array<char, 512> buf;
        std::unique_ptr<FILE, decltype(&detail::pclose_wrap)> pipe(
            detail::popen_wrap(cmd.c_str(), "r"), detail::pclose_wrap);
        if (!pipe) return info;
        std::string output;
        while (fgets(buf.data(), static_cast<int>(buf.size()), pipe.get()))
            output += buf.data();
        // CSV パース: "width,height,r_frame_rate_num/den,codec_name,nb_frames\nduration"
        // ffprobe の出力は改行区切りで複数行の場合がある
        if (std::sscanf(output.c_str(), "%d,%d,", &info.width, &info.height) >= 2) {
            // fps: "30/1" or "30000/1001" 形式
            auto pos = output.find(',', output.find(',') + 1);
            if (pos != std::string::npos) {
                int num = 0, den = 1;
                if (std::sscanf(output.c_str() + pos + 1, "%d/%d", &num, &den) >= 1) {
                    info.fps = (den > 0) ? static_cast<double>(num) / den : 30.0;
                }
            }
        }
        return info;
    }
};

/// 動画リーダー (ffmpeg CLI パイプ)
/// 任意のコーデック/コンテナを RGB24 rawvideo としてデコード
template<typename T>
class VideoReader {
public:
    explicit VideoReader(const std::string& path) : path_(path) {
        info_ = VideoInfo::probe(path);
        if (info_.width <= 0 || info_.height <= 0)
            throw std::runtime_error("VideoReader: cannot probe " + path);

        // ffmpeg → rawvideo RGB24 をパイプで読む
        std::string cmd = "ffmpeg -v error -i \"" + path
            + "\" -f rawvideo -pix_fmt rgb24 -";
#ifdef _WIN32
        cmd += " 2>nul";
        pipe_.reset(detail::popen_wrap(cmd.c_str(), "rb"));
#else
        cmd += " 2>/dev/null";
        pipe_.reset(detail::popen_wrap(cmd.c_str(), "r"));
#endif
        if (!pipe_)
            throw std::runtime_error("VideoReader: cannot start ffmpeg");

        frameBytes_ = static_cast<size_t>(info_.width) * info_.height * 3;
        buf_.resize(frameBytes_);
    }

    int width() const { return info_.width; }
    int height() const { return info_.height; }
    double fps() const { return info_.fps; }
    const VideoInfo& info() const { return info_; }

    /// 次のフレームを読み込み (EOF で nullopt)
    std::optional<Image<T>> readFrame() {
        size_t read = std::fread(buf_.data(), 1, frameBytes_, pipe_.get());
        if (read < frameBytes_) return std::nullopt;

        Image<T> frame(info_.height, info_.width, 3);
        for (int y = 0; y < info_.height; ++y) {
            for (int x = 0; x < info_.width; ++x) {
                size_t off = (static_cast<size_t>(y) * info_.width + x) * 3;
                if constexpr (std::is_same_v<T, uint8_t>) {
                    frame.at(y, x, 0) = buf_[off];
                    frame.at(y, x, 1) = buf_[off + 1];
                    frame.at(y, x, 2) = buf_[off + 2];
                } else {
                    frame.at(y, x, 0) = T(buf_[off])     / T(255);
                    frame.at(y, x, 1) = T(buf_[off + 1]) / T(255);
                    frame.at(y, x, 2) = T(buf_[off + 2]) / T(255);
                }
            }
        }
        return frame;
    }

    /// 全フレーム読み込み
    std::vector<Image<T>> readAll() {
        std::vector<Image<T>> frames;
        while (auto f = readFrame()) frames.push_back(std::move(*f));
        return frames;
    }

private:
    std::string path_;
    VideoInfo info_;
    std::unique_ptr<FILE, decltype(&detail::pclose_wrap)> pipe_{nullptr, detail::pclose_wrap};
    size_t frameBytes_ = 0;
    std::vector<uint8_t> buf_;
};

/// 動画ライターのオプション
struct VideoWriterOptions {
    std::string codec = "libx264";   // エンコーダ名
    int crf = 23;                    // 品質 (0=ロスレス, 51=最低品質)
    std::string preset = "medium";   // エンコード速度 (ultrafast/fast/medium/slow/veryslow)
    std::string pixelFormat = "yuv420p"; // 出力ピクセルフォーマット
    std::string subtitleFile;        // 字幕ファイル (.srt/.ass) — 空なら字幕なし
    std::string extraArgs;           // 追加の ffmpeg 引数
};

/// 動画ライター (ffmpeg CLI パイプ)
/// RGB24 rawvideo → ffmpeg で任意コーデックにエンコード
class VideoWriter {
public:
    VideoWriter(const std::string& path, int width, int height, int fps = 30,
                const VideoWriterOptions& opt = {})
        : width_(width), height_(height) {

        std::string cmd = "ffmpeg -v error -y"
            " -f rawvideo -pix_fmt rgb24"
            " -s " + std::to_string(width) + "x" + std::to_string(height) +
            " -r " + std::to_string(fps) +
            " -i -";  // stdin から rawvideo を読む

        // 字幕ファイルがあれば入力に追加
        if (!opt.subtitleFile.empty()) {
            cmd += " -i \"" + opt.subtitleFile + "\"";
        }

        // エンコード設定
        cmd += " -c:v " + opt.codec;
        if (opt.codec == "libx264" || opt.codec == "libx265") {
            cmd += " -crf " + std::to_string(opt.crf);
            cmd += " -preset " + opt.preset;
        }
        cmd += " -pix_fmt " + opt.pixelFormat;

        // 字幕の埋め込み
        if (!opt.subtitleFile.empty()) {
            // MP4 の場合は mov_text、MKV の場合は srt/ass をそのまま
            if (path.size() >= 4 && path.substr(path.size() - 4) == ".mkv") {
                cmd += " -c:s copy";
            } else {
                cmd += " -c:s mov_text";
            }
        }

        if (!opt.extraArgs.empty()) {
            cmd += " " + opt.extraArgs;
        }

        cmd += " \"" + path + "\"";

#ifdef _WIN32
        cmd += " 2>nul";
        pipe_.reset(detail::popen_wrap(cmd.c_str(), "wb"));
#else
        cmd += " 2>/dev/null";
        pipe_.reset(detail::popen_wrap(cmd.c_str(), "w"));
#endif
        if (!pipe_)
            throw std::runtime_error("VideoWriter: cannot start ffmpeg");

        frameBytes_ = static_cast<size_t>(width) * height * 3;
        buf_.resize(frameBytes_);
    }

    /// フレームを追加
    template<typename T>
    void addFrame(const Image<T>& frame) {
        if (!pipe_) return;
        for (int y = 0; y < height_; ++y) {
            for (int x = 0; x < width_; ++x) {
                size_t off = (static_cast<size_t>(y) * width_ + x) * 3;
                if (y < static_cast<int>(frame.height()) && x < static_cast<int>(frame.width())) {
                    if (frame.numChannels() >= 3) {
                        buf_[off]     = detail::toU8(frame.at(y, x, 0));
                        buf_[off + 1] = detail::toU8(frame.at(y, x, 1));
                        buf_[off + 2] = detail::toU8(frame.at(y, x, 2));
                    } else {
                        uint8_t v = detail::toU8(frame.at(y, x));
                        buf_[off] = buf_[off + 1] = buf_[off + 2] = v;
                    }
                } else {
                    buf_[off] = buf_[off + 1] = buf_[off + 2] = 0;
                }
            }
        }
        std::fwrite(buf_.data(), 1, frameBytes_, pipe_.get());
    }

    /// ファイルを閉じる
    void close() { pipe_.reset(); }

    ~VideoWriter() { close(); }

private:
    int width_, height_;
    std::unique_ptr<FILE, decltype(&detail::pclose_wrap)> pipe_{nullptr, detail::pclose_wrap};
    size_t frameBytes_ = 0;
    std::vector<uint8_t> buf_;
};

/// 動画ファイルの変換/処理 (CLI 呼び出し)
/// フィルタ、字幕合成、再エンコード等を 1 コマンドで実行
/// @param args  ffmpeg 引数の文字列 (例: "-i in.mp4 -vf subtitles=sub.srt -c:v libx264 out.mp4")
/// @return      ffmpeg の終了コード (0 = 成功)
inline int ffmpegExec(const std::string& args) {
    std::string cmd = "ffmpeg " + args;
    return std::system(cmd.c_str());
}

/// 字幕を動画に焼き付ける (hardsub)
/// @param input     入力動画パス
/// @param subtitle  字幕ファイルパス (.srt / .ass)
/// @param output    出力動画パス
/// @param opt       エンコードオプション
inline int burnSubtitles(const std::string& input, const std::string& subtitle,
                          const std::string& output, const VideoWriterOptions& opt = {}) {
    std::string cmd = "-v error -y -i \"" + input + "\""
        " -vf \"subtitles='" + subtitle + "'\""
        " -c:v " + opt.codec;
    if (opt.codec == "libx264" || opt.codec == "libx265") {
        cmd += " -crf " + std::to_string(opt.crf) + " -preset " + opt.preset;
    }
    cmd += " -c:a copy \"" + output + "\"";
    return ffmpegExec(cmd);
}

} // namespace calx::image

#endif // CALX_IMAGE_IO_VIDEO_HPP
