#include "BmpReader.h"
#include "Colormodes.h"
#include "BMP.h"
#include <fstream>
#include <cstdint>

#ifdef _BMPLIB_DEBUG_OUTPUT_
#include <iostream>
#endif

namespace Leonetienne::BmpPP {

    bool BmpReader::Read(BMP &image, const std::string &filename) {
        // Open file
        std::ifstream ifs(filename, std::ifstream::binary);
        if (!ifs.good())
            return false;

        // When reading the image, we don't care about most of the header.
        // All we want is the images resolution, bits-per-pixel, and the pixelbuffer address...
        // This is because we do not support ALL of bmp, but just the most common formats...

        // Fetch bmp signature
        std::uint16_t signature;
        ReadBytes(ifs, signature);

        // Does the signature match?
        if (signature != 0x4D42) {
            #ifdef _BMPLIB_DEBUG_OUTPUT_
            std::cerr << "Bmp signature doesn't match" << std::endl;
            #endif
            ifs.close();
            return false;
        }

        // Skip eight useless bytes (filesize + reserved0 + reserved1)
        ifs.ignore(8);

        // Read the address of the pixel buffer
        std::uint32_t pixelBufferAddress;
        ReadBytes(ifs, pixelBufferAddress);

        // Read the size of the dib header
        std::uint32_t dibHeaderSize;
        ReadBytes(ifs, dibHeaderSize);

        std::uint32_t width;
        std::uint32_t height;
        std::uint16_t bitsPerPixel;

        // Check for what kind of dib header we're dealing with.
        // Is it a BITMAPCOREHEADER or a variant of a BITMAPINFOHEADER?
        if (dibHeaderSize == 12) {
            // BITMAPCOREHEADER it is

            // Read the width and the height (both 2-byte ints)
            std::uint16_t width_2byte;
            std::uint16_t height_2byte;

            ifs >> width_2byte;
            ifs >> height_2byte;
            ReadBytes(ifs, width_2byte);
            ReadBytes(ifs, height_2byte);

            width = width_2byte;
            height = height_2byte;

            // Read the bits per pixel
            ReadBytes(ifs, bitsPerPixel);
        }
        else if (
                (dibHeaderSize == 40) ||
                (dibHeaderSize == 52) ||
                (dibHeaderSize == 56) ||
                (dibHeaderSize == 124) ||
                (dibHeaderSize == 108)
                ) {
            // This routine should work for all the variants of BITMAPINFOHEADERs

            // Read the width and height
            ReadBytes(ifs, width);
            ReadBytes(ifs, height);

            // Skip two useless bytes
            ifs.ignore(2);

            // Read the bits per pixel value
            ReadBytes(ifs, bitsPerPixel);
        }
        else {
            #ifdef _BMPLIB_DEBUG_OUTPUT_
            std::cerr << "Unsupported dib header found. Dib header size: " << dibHeaderSize << " bytes." << std::endl;
            #endif
            ifs.close();
            return false;
        }

        // Convert bits per pixel to color mode
        Colormode colormode;
        switch (bitsPerPixel) {
            case 32:
                colormode = Colormode::RGBA;
                break;

            case 24:
                colormode = Colormode::RGB;
                break;

            default:
                #ifdef _BMPLIB_DEBUG_OUTPUT_
                std::cerr << "Unsupported pixel format found (bits per pixel). Bits per pixel: " << bitsPerPixel << std::endl;
                #endif
                ifs.close();
                return false;
        }

        // Now read the pixel array
        // Pre-calculate the channel count
        const std::size_t numChannels = bitsPerPixel / 8;

        // Calculate how much padding there should be between each row
        const std::size_t paddingBytesPerRow = (4 - ((width * numChannels) % 4)) % 4;

        // Create the pixel buffer
        std::vector<std::uint8_t> pixelArray;
        pixelArray.resize(width * height * numChannels);

        // Set the reading-head to the address of the pixel array
        ifs.seekg(pixelBufferAddress);

        // Iterate over all rows, and skip padding after each row
        for (std::int64_t y = height-1; y >= 0; y--) {
            const std::size_t rowIndex = y * width * numChannels;

            for (std::size_t x = 0; x < width; x++) {
                const std::size_t pixelIndex = rowIndex + x * numChannels;

                switch (colormode) {
                    case Colormode::RGBA: {
                        std::uint8_t r, g, b, a;
                        ReadBytes(ifs, b);
                        ReadBytes(ifs, g);
                        ReadBytes(ifs, r);
                        ReadBytes(ifs, a);

                        pixelArray[pixelIndex + 0] = r;
                        pixelArray[pixelIndex + 1] = g;
                        pixelArray[pixelIndex + 2] = b;
                        pixelArray[pixelIndex + 3] = a;
                    }
                    break;

                    case Colormode::RGB: {
                        std::uint8_t r, g, b;
                        ReadBytes(ifs, b);
                        ReadBytes(ifs, g);
                        ReadBytes(ifs, r);

                        pixelArray[pixelIndex + 0] = r;
                        pixelArray[pixelIndex + 1] = g;
                        pixelArray[pixelIndex + 2] = b;
                    }
                    break;
                }
            }

            if (paddingBytesPerRow > 0)
                ifs.ignore(paddingBytesPerRow);
        }

        // We are done with the file
        ifs.close();

        // Now construct our image
        image.ReInitialize(Eule::Vector2i(width, height), colormode);

        // We're a friend-class, so we're going to MOVE (not copy) the pixels over manually. Much faster.
        image.pixelBuffer.swap(pixelArray);

        // Done.
        return true;
    }

}
