/*
    MapRoundingTool.c
    -----------------
    A C program that processes .map files (or any file) on Windows.
    It “rounds” numeric tokens in the file according to the following rules:

    1. By default (with no -p flag), if a token is written in standard notation
       (i.e. without an 'e' or 'E'):
         - If a run of at least three consecutive zeros is found in its fractional part
           (after at least one nonzero digit), the token is truncated at that point.
           For example:
             - "-0.61000000009" becomes "-0.61"
             - "3.2901920001" becomes "3.290192"
         - Otherwise, if a run of at least three consecutive nines is found in the fractional part,
           the token is rounded to the number of decimals preceding the run.
           For example:
             - "-0.9749999999999998" becomes "-0.975"
         - If neither condition is met, the token remains unchanged.
       For tokens in scientific notation (containing 'e' or 'E'):
         - If fabs(val) < 1e-6, output "0"; otherwise, leave unchanged.
    
    2. With the optional precision flag (-precision or -p), every numeric token is 
       rounded to the specified number of decimal places using standard rounding.
       For example, with “-p 4” the token “2.123456789” becomes “2.1235”.

    3. File/Directory handling:
         - No argument: processes all .map files in the current directory.
         - A directory path: processes all .map files in that directory.
         - A full file path: processes that file regardless of extension.

    4. Flags:
         - -verbose      : show line‐by‐line changes.
         - -silent or -s : do not prompt for commit and do not pause for key input.

    5. Timing and user confirmation:
         - The program prints the number of edits and processing time (seconds+milliseconds) for each file.
         - If changes were made and not in silent mode, it prompts “Press Y to commit …”.
         - If no prompt was given (either because no edits occurred or silent mode is active),
           the program pauses for a key input before closing (unless silent mode is enabled).

    Compile with your favorite Windows C compiler.
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#include <time.h>
#include <windows.h>

#define INIT_OUT_SIZE 16384

// Global options set via command-line.
static int g_verbose = 0;       // if 1, show line-by-line changes
static int g_silent = 0;        // if 1, do not prompt or pause
static int g_precision = -1;    // if >=0, round all numbers to this many decimals

// Append a string to a dynamic buffer, growing it if needed.
static void append_str(char **dest, size_t *destSize, size_t *destLen, const char *src) {
    size_t srcLen = strlen(src);
    if (*destLen + srcLen + 1 > *destSize) {
        *destSize = (*destSize) * 2 + srcLen;
        *dest = (char *)realloc(*dest, *destSize);
        if (!*dest) {
            fprintf(stderr, "Memory allocation error\n");
            exit(EXIT_FAILURE);
        }
    }
    strcpy(*dest + *destLen, src);
    *destLen += srcLen;
}

/*
    process_number_token():
    -----------------------
    Given a numeric token (as a string) and its double value, returns a new allocated
    string representing the “rounded” version according to these rules:
    
    If global precision (g_precision) is specified, the number is rounded to that many decimals.
    
    Otherwise, for tokens in scientific notation (containing 'e' or 'E'):
         - If fabs(val) < 1e-6, return "0"; otherwise, leave unchanged.
    
    Otherwise (standard notation):
         - First, search for a run of at least 3 consecutive zeros in the fractional part 
           (starting after the first digit of the fractional part). If found, truncate the token 
           at that point. If the resulting fractional part is entirely zeros, output just the integer part.
         - If no zeros–run is found, then search for a run of at least 3 consecutive nines.
           If found, round the value to the number of decimals equal to the index where the nines begin.
         - If neither condition is met, the token is left unchanged.
*/
static char *process_number_token(const char *token, double val) {
    char buffer[64];
    
    if (g_precision >= 0) {
        // Global precision specified: standard rounding.
        double factor = pow(10, g_precision);
        double rounded = round(val * factor) / factor;
        snprintf(buffer, sizeof(buffer), "%.*f", g_precision, rounded);
        // Remove trailing zeros and a trailing decimal point.
        char *p = buffer + strlen(buffer) - 1;
        while (p > buffer && *p == '0') { *p-- = '\0'; }
        if (p > buffer && *p == '.') { *p = '\0'; }
        return _strdup(buffer);
    } else {
        // No global precision specified.
        if (strchr(token, 'e') || strchr(token, 'E')) {
            // Scientific notation: if the value is extremely small, output "0".
            if (fabs(val) < 1e-6)
                strcpy(buffer, "0");
            else
                strcpy(buffer, token);
            return _strdup(buffer);
        } else {
            // Standard notation.
            char *dot = strchr(token, '.');
            if (!dot) {
                // No decimal point; leave unchanged.
                strcpy(buffer, token);
                return _strdup(buffer);
            }
            const char *frac = dot + 1;
            int fracLen = (int)strlen(frac);
            int zeros_found = 0, zeros_index = 0;
            // Look for a run of at least 3 consecutive zeros, starting after the first fractional digit.
            for (int i = 1; i <= fracLen - 3; i++) {
                if (frac[i] == '0' && frac[i+1] == '0' && frac[i+2] == '0') {
                    zeros_found = 1;
                    zeros_index = i;
                    break;
                }
            }
            if (zeros_found) {
                // Truncate at the start of the zeros run.
                int intPartLen = (int)(dot - token);
                char intPart[32];
                strncpy(intPart, token, intPartLen);
                intPart[intPartLen] = '\0';
                
                char newFrac[64];
                int copyLen = zeros_index;  // copy digits before the zeros run
                if (copyLen >= (int)sizeof(newFrac))
                    copyLen = sizeof(newFrac) - 1;
                strncpy(newFrac, frac, copyLen);
                newFrac[copyLen] = '\0';
                
                // If the fractional part becomes empty or all zeros, output only the integer part.
                int allZeros = 1;
                for (int i = 0; i < copyLen; i++) {
                    if (newFrac[i] != '0') { allZeros = 0; break; }
                }
                if (allZeros) {
                    strcpy(buffer, intPart);
                } else {
                    snprintf(buffer, sizeof(buffer), "%s.%s", intPart, newFrac);
                }
                return _strdup(buffer);
            } else {
                // Look for a run of at least 3 consecutive nines.
                int nines_found = 0, nines_index = 0;
                for (int i = 0; i <= fracLen - 3; i++) {
                    if (frac[i] == '9' && frac[i+1] == '9' && frac[i+2] == '9') {
                        nines_found = 1;
                        nines_index = i;
                        break;
                    }
                }
                if (nines_found) {
                    // Round to the number of decimals equal to the index where the nines begin.
                    int decimals = nines_index;
                    double factor = pow(10, decimals);
                    double roundedVal = round(val * factor) / factor;
                    snprintf(buffer, sizeof(buffer), "%.*f", decimals, roundedVal);
                    // Remove trailing zeros and a trailing dot.
                    char *p = buffer + strlen(buffer) - 1;
                    while (p > buffer && *p == '0') { *p-- = '\0'; }
                    if (p > buffer && *p == '.') { *p = '\0'; }
                    return _strdup(buffer);
                } else {
                    // No rounding rule triggered; leave unchanged.
                    strcpy(buffer, token);
                    return _strdup(buffer);
                }
            }
        }
    }
}

/*
    process_file():
    ---------------
    Reads the entire file, processes every numeric token, and writes the new content
    to a temporary buffer. For each token that is “rounded” (i.e. changed), if verbose mode (-verbose)
    is enabled, the change is printed with its line number.
    
    After processing, if any changes were made and prompting is enabled, the user is asked
    (with “Press Y to commit …”). Otherwise, if no prompt is given, the program pauses for a key
    input before continuing (unless in silent mode).
    
    Returns the number of edits made.
*/
static int process_file(const char *filename, int promptForCommit) {
    FILE *fin = fopen(filename, "rb");
    if (!fin) {
        fprintf(stderr, "Error opening file: %s\n", filename);
        return -1;
    }
    
    // Determine file size.
    fseek(fin, 0, SEEK_END);
    long fsize = ftell(fin);
    rewind(fin);
    
    char *inBuffer = (char *)malloc(fsize + 1);
    if (!inBuffer) {
        fprintf(stderr, "Memory allocation error\n");
        fclose(fin);
        return -1;
    }
    fread(inBuffer, 1, fsize, fin);
    inBuffer[fsize] = '\0';
    fclose(fin);
    
    // Prepare an output buffer.
    size_t outSize = INIT_OUT_SIZE;
    size_t outLen = 0;
    char *outBuffer = (char *)malloc(outSize);
    if (!outBuffer) {
        fprintf(stderr, "Memory allocation error\n");
        free(inBuffer);
        return -1;
    }
    outBuffer[0] = '\0';
    
    int edits = 0;
    int lineNum = 1;
    const char *p = inBuffer;
    clock_t fileStart = clock();
    
    // Process input buffer character-by-character.
    while (*p) {
        // Count newlines.
        if (*p == '\n') {
            append_str(&outBuffer, &outSize, &outLen, "\n");
            lineNum++;
            p++;
            continue;
        }
        
        // Check if the current character might be the start of a number.
        if (isdigit(*p) ||
            ((*p == '+' || *p == '-') && (isdigit(*(p+1)) || *(p+1)=='.')) ||
            (*p == '.' && isdigit(*(p+1)))) {
            
            char *endPtr;
            double origVal = strtod(p, &endPtr);
            if (p == endPtr) {
                // Not a valid number; copy one character.
                char tmp[2] = { *p, '\0' };
                append_str(&outBuffer, &outSize, &outLen, tmp);
                p++;
            } else {
                size_t tokenLen = endPtr - p;
                char *origToken = (char *)malloc(tokenLen + 1);
                strncpy(origToken, p, tokenLen);
                origToken[tokenLen] = '\0';
                
                char *newToken = process_number_token(origToken, origVal);
                if (strcmp(origToken, newToken) != 0) {
                    // If verbose mode is enabled, show the line-by-line change.
                    if (g_verbose) {
                        printf("Changed: '%s' -> '%s' (line %d)\n", origToken, newToken, lineNum);
                    }
                    edits++;
                }
                append_str(&outBuffer, &outSize, &outLen, newToken);
                
                free(origToken);
                free(newToken);
                p = endPtr;
            }
        } else {
            char tmp[2] = { *p, '\0' };
            append_str(&outBuffer, &outSize, &outLen, tmp);
            p++;
        }
    }
    
    clock_t fileEnd = clock();
    double elapsedSec = ((double)(fileEnd - fileStart)) / CLOCKS_PER_SEC;
    printf("File: %s  Edits: %d  Time: %.3f seconds\n", filename, edits, elapsedSec);
    
    int doCommit = 1;
    if (promptForCommit && edits > 0) {
        printf("Press Y to commit changes to '%s': ", filename);
        int ch = getchar();
        if (ch != 'Y' && ch != 'y') {
            doCommit = 0;
            printf("Changes not saved for '%s'.\n", filename);
        }
        while (ch != '\n' && ch != EOF)
            ch = getchar();
    }
    
    if (doCommit && edits > 0) {
        FILE *fout = fopen(filename, "wb");
        if (!fout) {
            fprintf(stderr, "Error writing to file: %s\n", filename);
        } else {
            fwrite(outBuffer, 1, outLen, fout);
            fclose(fout);
            printf("Changes saved to '%s'.\n", filename);
        }
    }
    
    // If no commit prompt was given, pause for a key press (unless in silent mode).
    if (!g_silent && (!promptForCommit || edits == 0)) {
        printf("Press any key to continue for '%s'...", filename);
        getchar();
    }
    
    free(inBuffer);
    free(outBuffer);
    return edits;
}

// Helper: Check if a given path is a directory.
static int is_directory(const char *path) {
    DWORD attr = GetFileAttributes(path);
    if (attr == INVALID_FILE_ATTRIBUTES)
        return 0;
    return (attr & FILE_ATTRIBUTE_DIRECTORY) != 0;
}

// Process all .map files in the given directory.
static int process_directory(const char *dirPath, int promptForCommit) {
    char searchPath[MAX_PATH];
    snprintf(searchPath, MAX_PATH, "%s\\*.map", dirPath);
    
    WIN32_FIND_DATA ffd;
    HANDLE hFind = FindFirstFile(searchPath, &ffd);
    if (hFind == INVALID_HANDLE_VALUE) {
        fprintf(stderr, "No .map files found in directory: %s\n", dirPath);
        return 0;
    }
    
    int totalEdits = 0;
    clock_t overallStart = clock();
    do {
        char fullPath[MAX_PATH];
        snprintf(fullPath, MAX_PATH, "%s\\%s", dirPath, ffd.cFileName);
        int fileEdits = process_file(fullPath, !g_silent);
        if (fileEdits >= 0)
            totalEdits += fileEdits;
    } while (FindNextFile(hFind, &ffd));
    FindClose(hFind);
    
    clock_t overallEnd = clock();
    double elapsedSec = ((double)(overallEnd - overallStart)) / CLOCKS_PER_SEC;
    printf("Overall processing time: %.3f seconds\n", elapsedSec);
    return totalEdits;
}

int main(int argc, char *argv[]) {
    char *targetPath = NULL;
    
    // Parse command-line arguments.
    for (int i = 1; i < argc; i++) {
        if (_stricmp(argv[i], "-verbose") == 0) {
            g_verbose = 1;
        } else if (_stricmp(argv[i], "-silent") == 0 || _stricmp(argv[i], "-s") == 0) {
            g_silent = 1;
        } else if (_stricmp(argv[i], "-precision") == 0 || _stricmp(argv[i], "-p") == 0) {
            if (i + 1 < argc) {
                g_precision = atoi(argv[i+1]);
                i++;
            } else {
                fprintf(stderr, "Error: -precision flag requires a numeric argument.\n");
                return 1;
            }
        } else {
            targetPath = argv[i];
        }
    }
    
    if (targetPath == NULL) {
        // No file/directory specified – process all .map files in the current directory.
        char currentDir[MAX_PATH];
        GetCurrentDirectory(MAX_PATH, currentDir);
        printf("No target specified. Processing .map files in current directory: %s\n", currentDir);
        process_directory(currentDir, !g_silent);
    } else {
        if (is_directory(targetPath)) {
            process_directory(targetPath, !g_silent);
        } else {
            process_file(targetPath, !g_silent);
        }
    }
    
    return 0;
}
