//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
//! https://learn.microsoft.com/en-us/previous-versions//dd183376(v=vs.85)
//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo
//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader
//! https://archive.org/details/mac_Graphics_File_Formats_Second_Edition_1996/page/n607/mode/2up
//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header
//!
//! Notes:
//! - The Microsoft documentation is incredibly unclear about the color table when the
//!   bit depth is >= 16.
//!   + For bit depth 24 it says "the bmiColors member of BITMAPINFO is NULL" but also
//!     says "the bmiColors color table is used for optimizing colors used on palette-based
//!     devices, and must contain the number of entries specified by the bV5ClrUsed member"
//!   + For bit depth 16 and 32, it seems to imply that if the compression is BI_BITFIELDS
//!     or BI_ALPHABITFIELDS, then the color table *only* consists of the bit masks, but
//!     doesn't really say this outright and the Wikipedia article seems to disagree
//!   For the purposes of this implementation, color tables can always be present for any
//!   bit depth and compression, and the color table follows the header + any optional
//!   bit mask fields dictated by the specified compression.

const std = @import("std");
const BitmapHeader = @import("ico.zig").BitmapHeader;
const builtin = @import("builtin");
const native_endian = builtin.cpu.arch.endian();

pub const windows_format_id = std.mem.readInt(u16, "BM", native_endian);
pub const file_header_len = 14;

pub const ReadError = error{
    ReadFailed,
    UnexpectedEOF,
    InvalidFileHeader,
    ImpossiblePixelDataOffset,
    UnknownBitmapVersion,
    InvalidBitsPerPixel,
    TooManyColorsInPalette,
    MissingBitfieldMasks,
};

pub const BitmapInfo = struct {
    dib_header_size: u32,
    /// Contains the interpreted number of colors in the palette (e.g.
    /// if the field's value is zero and the bit depth is <= 8, this
    /// will contain the maximum number of colors for the bit depth
    /// rather than the field's value directly).
    colors_in_palette: u32,
    bytes_per_color_palette_element: u8,
    pixel_data_offset: u32,
    compression: Compression,

    pub fn getExpectedPaletteByteLen(self: *const BitmapInfo) u64 {
        return @as(u64, self.colors_in_palette) * self.bytes_per_color_palette_element;
    }

    pub fn getActualPaletteByteLen(self: *const BitmapInfo) u64 {
        return self.getByteLenBetweenHeadersAndPixels() - self.getBitmasksByteLen();
    }

    pub fn getByteLenBetweenHeadersAndPixels(self: *const BitmapInfo) u64 {
        return @as(u64, self.pixel_data_offset) - self.dib_header_size - file_header_len;
    }

    pub fn getBitmasksByteLen(self: *const BitmapInfo) u8 {
        // Only BITMAPINFOHEADER (3.1) has trailing bytes for the BITFIELDS
        // The 2.0 format doesn't have a compression field and 4.0+ has dedicated
        // fields for the masks in the header.
        const dib_version = BitmapHeader.Version.get(self.dib_header_size);
        return switch (dib_version) {
            .@"nt3.1" => switch (self.compression) {
                .BI_BITFIELDS => 12,
                .BI_ALPHABITFIELDS => 16,
                else => 0,
            },
            else => 0,
        };
    }

    pub fn getMissingPaletteByteLen(self: *const BitmapInfo) u64 {
        if (self.getActualPaletteByteLen() >= self.getExpectedPaletteByteLen()) return 0;
        return self.getExpectedPaletteByteLen() - self.getActualPaletteByteLen();
    }

    /// Returns the full byte len of the DIB header + optional bitmasks + color palette
    pub fn getExpectedByteLenBeforePixelData(self: *const BitmapInfo) u64 {
        return @as(u64, self.dib_header_size) + self.getBitmasksByteLen() + self.getExpectedPaletteByteLen();
    }

    /// Returns the full expected byte len
    pub fn getExpectedByteLen(self: *const BitmapInfo, file_size: u64) u64 {
        return self.getExpectedByteLenBeforePixelData() + self.getPixelDataLen(file_size);
    }

    pub fn getPixelDataLen(self: *const BitmapInfo, file_size: u64) u64 {
        return file_size - self.pixel_data_offset;
    }
};

pub fn read(reader: *std.Io.Reader, max_size: u64) ReadError!BitmapInfo {
    var bitmap_info: BitmapInfo = undefined;
    const file_header = reader.takeArray(file_header_len) catch |err| switch (err) {
        error.EndOfStream => return error.UnexpectedEOF,
        else => |e| return e,
    };

    const id = std.mem.readInt(u16, file_header[0..2], native_endian);
    if (id != windows_format_id) return error.InvalidFileHeader;

    bitmap_info.pixel_data_offset = std.mem.readInt(u32, file_header[10..14], .little);
    if (bitmap_info.pixel_data_offset > max_size) return error.ImpossiblePixelDataOffset;

    bitmap_info.dib_header_size = reader.takeInt(u32, .little) catch return error.UnexpectedEOF;
    if (bitmap_info.pixel_data_offset < file_header_len + bitmap_info.dib_header_size) return error.ImpossiblePixelDataOffset;
    const dib_version = BitmapHeader.Version.get(bitmap_info.dib_header_size);
    switch (dib_version) {
        .@"nt3.1", .@"nt4.0", .@"nt5.0" => {
            var dib_header_buf: [@sizeOf(BITMAPINFOHEADER)]u8 align(@alignOf(BITMAPINFOHEADER)) = undefined;
            std.mem.writeInt(u32, dib_header_buf[0..4], bitmap_info.dib_header_size, .little);
            reader.readSliceAll(dib_header_buf[4..]) catch |err| switch (err) {
                error.EndOfStream => return error.UnexpectedEOF,
                error.ReadFailed => |e| return e,
            };
            var dib_header: *BITMAPINFOHEADER = @ptrCast(&dib_header_buf);
            structFieldsLittleToNative(BITMAPINFOHEADER, dib_header);

            bitmap_info.colors_in_palette = try dib_header.numColorsInTable();
            bitmap_info.bytes_per_color_palette_element = 4;
            bitmap_info.compression = @enumFromInt(dib_header.biCompression);

            if (bitmap_info.getByteLenBetweenHeadersAndPixels() < bitmap_info.getBitmasksByteLen()) {
                return error.MissingBitfieldMasks;
            }
        },
        .@"win2.0" => {
            var dib_header_buf: [@sizeOf(BITMAPCOREHEADER)]u8 align(@alignOf(BITMAPCOREHEADER)) = undefined;
            std.mem.writeInt(u32, dib_header_buf[0..4], bitmap_info.dib_header_size, .little);
            reader.readSliceAll(dib_header_buf[4..]) catch |err| switch (err) {
                error.EndOfStream => return error.UnexpectedEOF,
                error.ReadFailed => |e| return e,
            };
            const dib_header: *BITMAPCOREHEADER = @ptrCast(&dib_header_buf);
            structFieldsLittleToNative(BITMAPCOREHEADER, dib_header);

            // > The size of the color palette is calculated from the BitsPerPixel value.
            // > The color palette has 2, 16, 256, or 0 entries for a BitsPerPixel of
            // > 1, 4, 8, and 24, respectively.
            bitmap_info.colors_in_palette = switch (dib_header.bcBitCount) {
                inline 1, 4, 8 => |bit_count| 1 << bit_count,
                24 => 0,
                else => return error.InvalidBitsPerPixel,
            };
            bitmap_info.bytes_per_color_palette_element = 3;

            bitmap_info.compression = .BI_RGB;
        },
        .unknown => return error.UnknownBitmapVersion,
    }

    return bitmap_info;
}

/// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader
pub const BITMAPCOREHEADER = extern struct {
    bcSize: u32,
    bcWidth: u16,
    bcHeight: u16,
    bcPlanes: u16,
    bcBitCount: u16,
};

/// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
pub const BITMAPINFOHEADER = extern struct {
    bcSize: u32,
    biWidth: i32,
    biHeight: i32,
    biPlanes: u16,
    biBitCount: u16,
    biCompression: u32,
    biSizeImage: u32,
    biXPelsPerMeter: i32,
    biYPelsPerMeter: i32,
    biClrUsed: u32,
    biClrImportant: u32,

    /// Returns error.TooManyColorsInPalette if the number of colors specified
    /// exceeds the number of possible colors referenced in the pixel data (i.e.
    /// if 1 bit is used per pixel, then the color table can't have more than 2 colors
    /// since any more couldn't possibly be indexed in the pixel data)
    ///
    /// Returns error.InvalidBitsPerPixel if the bit depth is not 1, 4, 8, 16, 24, or 32.
    pub fn numColorsInTable(self: BITMAPINFOHEADER) !u32 {
        switch (self.biBitCount) {
            inline 1, 4, 8 => |bit_count| switch (self.biClrUsed) {
                // > If biClrUsed is zero, the array contains the maximum number of
                // > colors for the given bitdepth; that is, 2^biBitCount colors
                0 => return 1 << bit_count,
                // > If biClrUsed is nonzero and the biBitCount member is less than 16,
                // > the biClrUsed member specifies the actual number of colors the
                // > graphics engine or device driver accesses.
                else => {
                    const max_colors = 1 << bit_count;
                    if (self.biClrUsed > max_colors) {
                        return error.TooManyColorsInPalette;
                    }
                    return self.biClrUsed;
                },
            },
            // > If biBitCount is 16 or greater, the biClrUsed member specifies
            // > the size of the color table used to optimize performance of the
            // > system color palettes.
            //
            // Note: Bit depths >= 16 only use the color table 'for optimizing colors
            // used on palette-based devices', but it still makes sense to limit their
            // colors since the pixel data is still limited to this number of colors
            // (i.e. even though the color table is not indexed by the pixel data,
            // the color table having more colors than the pixel data can represent
            // would never make sense and indicates a malformed bitmap).
            inline 16, 24, 32 => |bit_count| {
                const max_colors = 1 << bit_count;
                if (self.biClrUsed > max_colors) {
                    return error.TooManyColorsInPalette;
                }
                return self.biClrUsed;
            },
            else => return error.InvalidBitsPerPixel,
        }
    }
};

pub const Compression = enum(u32) {
    BI_RGB = 0,
    BI_RLE8 = 1,
    BI_RLE4 = 2,
    BI_BITFIELDS = 3,
    BI_JPEG = 4,
    BI_PNG = 5,
    BI_ALPHABITFIELDS = 6,
    BI_CMYK = 11,
    BI_CMYKRLE8 = 12,
    BI_CMYKRLE4 = 13,
    _,
};

fn structFieldsLittleToNative(comptime T: type, x: *T) void {
    inline for (@typeInfo(T).@"struct".fields) |field| {
        @field(x, field.name) = std.mem.littleToNative(field.type, @field(x, field.name));
    }
}

test "read" {
    var bmp_data = "BM<\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x10\x00\x00\x00\x00\x00\x06\x00\x00\x00\x12\x0b\x00\x00\x12\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x7f\x00\x00\x00\x00".*;
    var fbs: std.Io.Reader = .fixed(&bmp_data);

    {
        const bitmap = try read(&fbs, bmp_data.len);
        try std.testing.expectEqual(@as(u32, BitmapHeader.Version.@"nt3.1".len()), bitmap.dib_header_size);
    }

    {
        fbs.seek = 0;
        bmp_data[file_header_len] = 11;
        try std.testing.expectError(error.UnknownBitmapVersion, read(&fbs, bmp_data.len));

        // restore
        bmp_data[file_header_len] = BitmapHeader.Version.@"nt3.1".len();
    }

    {
        fbs.seek = 0;
        bmp_data[0] = 'b';
        try std.testing.expectError(error.InvalidFileHeader, read(&fbs, bmp_data.len));

        // restore
        bmp_data[0] = 'B';
    }

    {
        const cutoff_len = file_header_len + BitmapHeader.Version.@"nt3.1".len() - 1;
        var dib_cutoff_fbs: std.Io.Reader = .fixed(bmp_data[0..cutoff_len]);
        try std.testing.expectError(error.UnexpectedEOF, read(&dib_cutoff_fbs, bmp_data.len));
    }

    {
        const cutoff_len = file_header_len - 1;
        var bmp_cutoff_fbs: std.Io.Reader = .fixed(bmp_data[0..cutoff_len]);
        try std.testing.expectError(error.UnexpectedEOF, read(&bmp_cutoff_fbs, bmp_data.len));
    }
}
