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

// bmp.hpp — BMP 画像ファイルの読み書き
//
// 新規実装:
//   - 24bit/32bit BMP の読み込み・書き出し
//   - 外部ライブラリ不要
//   - Image<T> との連携

#ifndef CALX_IMAGE_IO_BMP_HPP
#define CALX_IMAGE_IO_BMP_HPP

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

namespace calx::image {

    // ── BMP ファイル構造体 (pack 制御) ──

#pragma pack(push, 1)
    struct BmpFileHeader {
        uint16_t type{0x4D42};    // "BM"
        uint32_t size{0};
        uint16_t reserved1{0};
        uint16_t reserved2{0};
        uint32_t offset{54};
    };

    struct BmpInfoHeader {
        uint32_t size{40};
        int32_t  width{0};
        int32_t  height{0};
        uint16_t planes{1};
        uint16_t bitCount{24};
        uint32_t compression{0};
        uint32_t imageSize{0};
        int32_t  xPelsPerMeter{2835};  // 72 DPI
        int32_t  yPelsPerMeter{2835};
        uint32_t colorsUsed{0};
        uint32_t colorsImportant{0};
    };
#pragma pack(pop)

    // ── ピクセル値 → uint8_t 変換ヘルパー ──

    namespace detail {
        template<typename T>
        inline uint8_t toU8(T v) {
            if constexpr (std::is_same_v<T, uint8_t>) {
                return v;
            } else if constexpr (std::is_floating_point_v<T>) {
                // [0, 1] → [0, 255]
                return static_cast<uint8_t>(std::clamp(v, T{0}, T{1}) * T{255} + T{0.5});
            } else {
                return static_cast<uint8_t>(std::clamp(static_cast<int>(v), 0, 255));
            }
        }

        template<typename T>
        inline T fromU8(uint8_t v) {
            if constexpr (std::is_same_v<T, uint8_t>) {
                return v;
            } else if constexpr (std::is_floating_point_v<T>) {
                return static_cast<T>(v) / T{255};
            } else {
                return static_cast<T>(v);
            }
        }
    }

    // ── BMP 書き出し ──

    /// Image<T> を 24bit BMP として保存
    /// 1ch 画像はグレースケール、3ch/4ch 画像は RGB として保存
    template<typename T>
    void saveBmp(const std::string& path, const Image<T>& img) {
        if (img.empty()) throw std::runtime_error("saveBmp: empty image");

        int w = static_cast<int>(img.width());
        int h = static_cast<int>(img.height());
        int rowBytes = ((w * 3 + 3) / 4) * 4;  // 4 バイトアライン

        BmpFileHeader fh;
        BmpInfoHeader ih;
        ih.width = w;
        ih.height = h;
        ih.bitCount = 24;
        ih.imageSize = rowBytes * h;
        fh.size = 54 + ih.imageSize;

        std::ofstream ofs(path, std::ios::binary);
        if (!ofs) throw std::runtime_error("saveBmp: cannot open " + path);

        ofs.write(reinterpret_cast<const char*>(&fh), sizeof(fh));
        ofs.write(reinterpret_cast<const char*>(&ih), sizeof(ih));

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

        // BMP は下から上の順
        for (int y = h - 1; y >= 0; --y) {
            for (int x = 0; x < w; ++x) {
                if (isColor) {
                    row[x * 3 + 0] = detail::toU8(img.at(y, x, 2)); // B
                    row[x * 3 + 1] = detail::toU8(img.at(y, x, 1)); // G
                    row[x * 3 + 2] = detail::toU8(img.at(y, x, 0)); // R
                } else {
                    uint8_t v = detail::toU8(img.at(y, x));
                    row[x * 3 + 0] = v;
                    row[x * 3 + 1] = v;
                    row[x * 3 + 2] = v;
                }
            }
            ofs.write(reinterpret_cast<const char*>(row.data()), rowBytes);
        }
    }

    /// Matrix<T> を BMP として保存 (グレースケール、後方互換)
    template<typename T>
    void saveBmp(const std::string& path, const Matrix<T>& mat) {
        saveBmp(path, Image<T>(mat));
    }

    // ── BMP 読み込み ──

    /// BMP ファイルを Image<T> として読み込み
    /// 24bit → 3ch、32bit → 4ch で読み込む
    template<typename T>
    Image<T> loadBmp(const std::string& path) {
        std::ifstream ifs(path, std::ios::binary);
        if (!ifs) throw std::runtime_error("loadBmp: cannot open " + path);

        BmpFileHeader fh;
        BmpInfoHeader ih;

        ifs.read(reinterpret_cast<char*>(&fh), sizeof(fh));
        if (fh.type != 0x4D42) throw std::runtime_error("loadBmp: not a BMP file");

        ifs.read(reinterpret_cast<char*>(&ih), sizeof(ih));

        if (ih.compression != 0)
            throw std::runtime_error("loadBmp: compressed BMP not supported");

        int w = ih.width;
        int h = std::abs(ih.height);
        bool topDown = (ih.height < 0);
        int bpp = ih.bitCount;

        if (bpp != 24 && bpp != 32)
            throw std::runtime_error("loadBmp: only 24/32 bit BMP supported");

        int channels = (bpp == 32) ? 4 : 3;
        int bytesPerPixel = bpp / 8;
        int rowBytes = ((w * bytesPerPixel + 3) / 4) * 4;

        Image<T> img(h, w, channels);

        ifs.seekg(fh.offset, std::ios::beg);
        std::vector<uint8_t> row(rowBytes);

        for (int i = 0; i < h; ++i) {
            int y = topDown ? i : (h - 1 - i);
            ifs.read(reinterpret_cast<char*>(row.data()), rowBytes);

            for (int x = 0; x < w; ++x) {
                int off = x * bytesPerPixel;
                img.at(y, x, 0) = detail::fromU8<T>(row[off + 2]); // R
                img.at(y, x, 1) = detail::fromU8<T>(row[off + 1]); // G
                img.at(y, x, 2) = detail::fromU8<T>(row[off + 0]); // B
                if (channels == 4) {
                    img.at(y, x, 3) = detail::fromU8<T>(row[off + 3]); // A
                }
            }
        }

        return img;
    }

} // namespace calx::image

#endif // CALX_IMAGE_IO_BMP_HPP
