/*
PROJECT: Terminal-Based PPM Image Renderer
ABOUT:
  This is a fully self-contained PPM (Portable Pixmap) image renderer
  that runs in the terminal

FEATURES:
  Renders PPM (P3) image files

REAL WORLD APPLICATION:
  This program can be used to view and display images directly in the terminal.
*/

#include 
#include 
#include 
#include 
#include 
#include 
#include 

// if we are on Windows include the Windows API header
// We need this because Windows' terminal for backwards compatibility reasons is ASCII by default
// And because we are using Unicode we have to tell it that
#ifdef _WIN32
    #include 
#endif

// Each pixel will be rendered using this block character.
// Using two characters makes the image look more square in the terminal.
#define PIXEL "██"

// Test image stored as a raw string literal.
// This allows testing the parser without loading a file.
#define TEST_FILE R"(P3
# Example PPM file for testing
4 4 255
255 0 0
255 0 255
0 255 0
0 255 255
128 128 128
255 128 0
64 64 64
0 0 255
192 192 192
255 64 64
255 255 0
255 255 255
0 0 0
0 128 255
128 0 255
64 255 64
)"

// A Pixel stores three integer values in this order: { Red, Green, Blue }
using Pixel = std::array;

// The Image structure holds all parsed data from a PPM file.
// width and height define the image size.
// max_value defines the maximum color intensity (usually 255).
// pixels stores all RGB values in row-major order.
struct Image {
    int width = 0;
    int height = 0;
    int max_value = 255;
    std::vector pixels;
};

/*
This is an internal helper to load files into a `std::string` object
*/
std::string _LoadFile(const std::string& filename) {
    std::ifstream file(filename);

    if (!file.is_open()) {
        std::cerr << "Error opening file: " << filename << std::endl;
        return ""; // Return empty string
    }
    
    std::stringstream buffer;
    buffer << file.rdbuf(); // Read the file's stream buffer into the stringstream
    file.close(); // Close the file stream
    return buffer.str(); // Convert the stringstream to a std::string
}

/*
This function reads a P3 formatted PPM image from a string
and fills the Image structure with parsed data.
Returns:
true -> if parsing succeeded
false -> if any errors occurred
*/
bool ParsePPM(const std::string& data, Image& img) {
    std::istringstream stream(data);
    std::string token;
    
    // Read and verify the magic number (must be P3 for ASCII PPM)
    stream >> token;
    
    if (token != "P3") { return false; }
    // Read width
    // If a comment is encountered (starts with #),
    // skip the rest of that line and continue.
    while (true) {
        stream >> token;
        if (token.size() > 0 && token[0] == '#') {
            std::getline(stream, token);
            continue;
        }
        img.width = std::stoi(token);
        break;
    }
    
    // Read height
    stream >> img.height;
    
    // Read maximum color value (usually 255)
    stream >> img.max_value;
    if (img.width <= 0 || img.height <= 0) return false;
    
    // Validate dimensions to prevent invalid or corrupted input
    int total_pixels = img.width * img.height;
    
    // Clear any previous image data
    img.pixels.clear();
    
    // Read every pixel in order.
    // Each pixel consists of 3 integers: R, G and B
    for (int i = 0; i < total_pixels; i++) {
        Pixel p;
        if (!(stream >> p[0])) return false;
        if (!(stream >> p[1])) return false;
        if (!(stream >> p[2])) return false;
        img.pixels.push_back(p);
    }
    return true;
}

/*
Prints a single pixel to the terminal using
ANSI 24-bit background color escape codes.
*/
void DrawPixel(const Pixel& pixel) {
    int r = pixel[0];
    int g = pixel[1];
    int b = pixel[2];

    // Set background color
    std::cout << "\x1b[38;2;"
    << r << ";"
    << g << ";"
    << b << "m"
    << PIXEL;

    // Reset formatting so colors don’t bleed
    std::cout << "\x1b[0m";
}

/*
This function will take the parsed image data and print it
to the terminal using ANSI escape codes for 24-bit color.
Each pixel is drawn as a colored block.
*/
void RenderImage(const Image& image) {

    for (int y = 0; y < image.height; y++) {
        for (int x = 0; x < image.width; x++) {
            int index = y * image.width + x;
            DrawPixel(image.pixels[index]);
        }

        // Move to next line after each row
        std::cout << "\n";
    }
}

/*
This function parses the TEST_FILE and renders it.
It allows testing without loading from disk.
*/
void RenderTestImage() {
    Image img;
    bool ok = ParsePPM(TEST_FILE, img);
    if (!ok) {
        std::cerr << "Error parsing test image\n";
        std::exit(-1);
    }
    RenderImage(img);
}

enum Mode {
    NORMAL,
    TEST,
};

/*
Determines which mode the program should run in
based on command-line arguments.
--test -> TEST mode
(none) -> NORMAL mode
*/
Mode GetMode(int argc, char** argv) {
    // Default mode
    Mode mode = NORMAL;
    // If at least one argument was provided
    if (argc > 1) {
        std::string arg = argv[1];
        if (arg == "--test") {
            mode = TEST;
        }
    }
    return mode;
}

int main(int argc, char** argv) {
    #ifdef _WIN32
    // Tell Windows we want the terminal to use Unicode Transformation Format 8bit
    SetConsoleOutputCP(CP_UTF8);
    #endif
    
    Mode mode = GetMode(argc, argv);
    
    switch (mode) {
        case NORMAL: {
            if (argc < 2) {
                std::cerr << "Usage: program \n";
                return -1;
            }
            
            // It didn't error so we can now load and render the PPM file
            std::string file_data = _LoadFile(argv[1]);
            Image img;
            ParsePPM(file_data, img);
            RenderImage(img);
            break;
        }
        
        case TEST: {
            // Render the test image to the terminal
            RenderTestImage();
            break;
        }
    }
    return 0;
}