/* snowc2c - Chris X Edwards - JUL 2026 */
/* Converts SnowC to C code -           */
/* Semantic Non-Optional Whitespace C   */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <getopt.h>

#define HERE(LINE) printf("HERE line:%d\n",LINE);

void usage(const char *prog) {
    printf("Usage: %s [options] file\n"
           "  -h --help\n"
           "  -i --indent N - Indent level. [default 2]\n"
           "  -m --map - Print parsed meta map.\n"
           "  -l --level - Print level diagnostics.\n"
           "  -d --debug\n",
           prog);
} // End usage()

typedef struct { int map; int indent; int level; int debug; } ConfigPack;
ConfigPack conf= {0}; /* Globally available. Initialize all to 0. */
ConfigPack parse_options(int argc, char *argv[]) {
    conf.indent= 2; /* Any non-zero default values. */
    int o;
    static struct option long_options[]= {
        {"help",   no_argument,       NULL, 'h'},
        {"map",    no_argument,       NULL, 'm'},
        {"level",  no_argument,       NULL, 'l'},
        {"indent", required_argument, NULL, 'i'},
        {"debug",  no_argument,       NULL, 'd'},
        {0, 0, 0, 0}
    }; // End struct long_options
    while (1) {
        o= getopt_long(argc, argv, "hmli:d", long_options, NULL);
        if (o == -1) break;
        switch (o) {
            case 'h': usage(argv[0]); exit(0);
            case 'm': conf.map= 1; break;
            case 'l': conf.level= 1; break;
            case 'i':
                conf.indent= atoi(optarg);
                if (conf.indent>0) break;
                else {
                    fprintf(stderr, "Indent invalid value: %d\n",conf.indent);
                    exit(1);
                }
            case 'd': conf.debug= (optarg) ? atoi(optarg) : 99; break;
            default:
                fprintf(stderr, "Unknown option: -%c\n", o);
                usage(argv[0]);
                exit(1);
        } // End switch
    }
    return conf;
} // End parse_options()

#define INDENTSIZE 4
// Define state modes.
#define IN_STR    (1 << 0)
#define IN_CHR    (1 << 1)
#define IN_CC_COM (1 << 2)
#define IN_C_COM  (1 << 3)
#define IN_PRE    (1 << 4)
#define IN_COM    (IN_CC_COM | IN_C_COM)

// Define markers which will fill meta map. Enum so they work in cases too.
enum { mC= 'C',   mSEC= ';',
       mSPC= '_', mEOL= '+',
       mCOM= '/', mPRE= '#',
       mLSP= '.', mTSP= ',',
       mSTR= '$', mCHR= '\''
     };

void map_major_parts(const char *buffer, size_t size, char *meta) {
    unsigned char state= 0;
    size_t i= 0;
    while (i < size) {
        char c= buffer[i];
        if (state & IN_COM) {                                    // In some kind of comment.
            if (state & IN_CC_COM && (c == '\n')) {              // EOL closing out a // comment?
                state &= ~IN_CC_COM;                             // Turn off CC comment mode.
                meta[i]= mEOL;                                   // Mark content of comment in meta.
            } else if (state & IN_C_COM && c == '*') {           // In C comment, possible close found.
                if (i + 1 < size && buffer[i + 1] == '/') {      // It is C comment closer.
                    state &= ~IN_C_COM;                          // Cancel C comment mode.
                    i++;                                         // One time index forward for both symbols.
                    meta[i-1]= mCOM;                             // In theory i-1 should always be > 0.
                    meta[i]= mCOM;                               // Mark second position in meta.
                } else {                                         // Just a * in a C comment.
                    meta[i]= mCOM;                               // Mark comment.
                }
            } else {
                if (buffer[i] == '\n') {
                    meta[i]= mEOL;                               // Mark EOL in a C comment.
                }
                meta[i]= mCOM;                                   // Mark content of comment in meta.
            }
        } else if (state & IN_STR) {                             // In a string literal.
            if (c == '"') {                                      // Escaped quotes checked - this must be end.
                state &= ~IN_STR;                                // Turn off string mode.
                meta[i]= mSTR;                                   // Mark this end quote's position as string.
            } else if (c == '\\' && i + 1 < size) {              // A backslash could escape the end quote.
                meta[i]= meta[i + 1]= mSTR;                      // Mark backslash and protectee.
                i++;                                             // Next.
            } else {
                meta[i]= mSTR;                                   // A normal string character.
            }
        } else if (state & IN_CHR) {                             // In character literal.
            if (c == '\'') {                                     // Found closing '.
                state &= ~IN_CHR;                                // Turn off character mode.
                meta[i]= mCHR;                                   // Mark char in meta with '.
            } else if (c == '\\' && i + 1 < size) {              // Found escaping backslash.
                meta[i]= meta[i + 1]= mCHR;                      // Mark backslash and protectee.
                i++;                                             // Next.
            } else {
                meta[i]= mCHR;                                   // Normal character closer.
            }
        } else if (state & IN_PRE) {                             // In preprocessor syntax.
            if (c == '\n') {                                     // May end PP macro.
                if (i > 0 && buffer[i - 1] != '\\') {            // Not an escaped line break, so the end.
                    state &= ~IN_PRE;                            // Turn off preprocessor mode.
                    meta[i]= mEOL;                               // Mark EOL.
                }
            } else {                                             // In pre mode but normal
                meta[i]= mPRE;                                   // Mark as preprocessor.
            }
        } else {                                                 // Not in any special mode.
            if (c == '"') {                                      // Must be a string literal starter.
                state |= IN_STR;                                 // Turn on string mode.
                meta[i]= mSTR;                                   // Mark string starter.
            } else if (c == '\'') {                              // Must be character starter.
                state |= IN_CHR;                                 // Turn on character mode.
                meta[i]= mCHR;                                   // Mark character starter.
            } else if (c == '/') {                               // Possible comment starter.
                if (i + 1 < size && buffer[i + 1] == '/') {      // Next is / so CC comment.
                    state |= IN_CC_COM;                          // Turn on CC comment mode.
                    meta[i]= meta[i + 1]= '/';                   // Mark CC starter.
                    i++;                                         // Index once for double syntax.
                } else if (i + 1 < size && buffer[i + 1] == '*') { // Must be C comment starter.
                    state |= IN_C_COM;                           // Turn on C comment mode.
                    meta[i]= meta[i + 1]= mCOM;                  // Mark C comment starter.
                    i++;                                         // Index once for double syntax.
                } else {
                    meta[i]= mC;                                 // Just an isolated division slash.
                }
            } else if (c == '#') {                               // Pre macro start possible.
                int a=i;                                         // A lookback counter.
                while (a>=0) {                                   // Don't go past start.
                    a--;                                         // Go back in buffer.
                    if (buffer[a] == ' ') {                      // If spaces before #, still candidate.
                        continue;                                // Try again yet one earlier.
                    } else if (buffer[a] == '\n' || (a<0)) {     // If hit prev newline or start of file.
                        state |= IN_PRE;                         // In PRE macro.
                        meta[i]= mPRE;                           // Mark start of PRE.
                        break;                                   // Done back searching.
                    } else {                                     // Found other char, so not a PRE macro.
                        break;                                   // Give up.
                    }
                }
            } else if (isspace(c)) {                             // Whitespace.
                if (c == '\n') {
                    meta[i]= mEOL;
                } else {
                    meta[i]= mSPC;                               // Hopefully kills tabs dead.
                }
            } else {
                meta[i]= mC;                                     // Some non-special regular C syntax.
            }
        }
        i++;                                                     // On to the next character.
    } // End while iterating through entire buffer to map.
} // End map_major_parts()

void map_leading_whitespace(const char *buffer, size_t size, char *meta) {
    unsigned char in_leading_space= 1; // Starts assuming first line might have leading space.
    size_t i= 0;
    while (i < size) {
        char c= buffer[i];
        if (in_leading_space && meta[i] != mCOM) {               // In leading spc - ignore comments.
            if (c == ' ' || c == '\t') {                         // Space or tab found.
                meta[i]= mLSP;                                   // Mark as leading space.
            } else if (c == '\n' ||                              // Line of all spaces. Stay in mode.
                c == '{' || c == '}' || c == ';') {              // Also ignorable for purpose of LSP.
            } else {                                             // Found end of leading space.
                in_leading_space= 0;                             // Turn off leading space mode.
            }
        } // End if in_leading_space.
        else {
            if (c == '\n') {                                     // NL, could start leading spaces.
                in_leading_space= 1;                             // Turn on leading space mode.
            }
        } // End else not in_leading_space.
        i++;
    } // End while not through entire text.
} // End map_leading_whitespace()

void map_trailing_whitespace(const char *buffer, size_t size, char *meta) {
    unsigned char in_trailing_space= 1; // Starts assuming first line might have trailing space.
    size_t i= size - 1;
    // Strange thing here that >= 0 will bork entire program!
    while (i > 0) {                          // Can't be first char anyway.
        char m= meta[i], b= buffer[i];
        if (in_trailing_space) {                                 // In trailing space.
            if (m == mSPC ) {                                    // Whitespace found.
                meta[i]= mTSP;                                   // Overwrite mark to trailing space.
            // All of these are also ignorable for the purpose of TSP.
            } else if (m == mEOL || m == mCOM || b == '{' || m == '}' || m == ';') {
            } else {                                             // Found end of trailing space.
                in_trailing_space= 0;                            // Turn off trailing space mode.
            }
        } // End if in_trailing_space.
        else {
            if (m == mEOL) {                                     // NL, could start trailing spaces.
                in_trailing_space= 1;                            // Turn on trailing space mode.
            }
        } // End else not in_trailing_space.
        i--;
    } // End while not through entire text.
} // End map_trailing_whitespace()

// Greatest Common Divisor - Figures out spaces per indent.
int gcd(int a, int b) {
    while (b) { int temp= b; b= a % b; a= temp; }
    return a;
} // End gcd()

int calc_spaces_per_indent(const char *buffer, const size_t size, const char *meta) {
    int ind_spcs= 0, this_indent= 0, first_indent= 0;
    for (int c=0; c<size; c++){
        if (meta[c] == mLSP) this_indent++;
        else if (meta[c] == mEOL) {
            if (!first_indent) first_indent= this_indent;
            ind_spcs= gcd(ind_spcs, this_indent);
            this_indent= 0; // Reset.
        } // End else if - found an EOL.
    } // End for c first pass through file.
    if (ind_spcs == 1 && first_indent > 1) { // Check for janky indents, then find out why.
        printf("Warning: Indent inconsistency. Using: %d\n",first_indent);
        int start= 0; int lineno= 0;
        for (int c=0; c<size; c++){ // Search again, now hunting for the bad indent lines.
            if (meta[c] == mLSP) this_indent++;
            else if (meta[c] == mEOL) {
                lineno++; // Track to help debugging.
                if (this_indent % first_indent != 0) { // Assumes 1st indent is right.
                    printf("Line: %d  Indented: %d\n",lineno,this_indent);
                    for(int c2= start; c2<c; c2++) {
                        putchar(buffer[c2]);
                    }
                    putchar('\n');
                }
                this_indent= 0; // Reset.
                start= c+1;
            } // End else if - found an EOL.
        } // End for c through entire file.
        return first_indent;
    } // End if indent inconsistency detected.
    return ind_spcs;
} // End calc_spaces_per_indent()

void print_annotated_buffer(const char *buffer, size_t size, const char *meta) {
    size_t i,m= 0;
    for (i= 0; i < size; i++) {
        if (buffer[i] == '\n') {
            putchar('\n');
            for (; m <= i; m++) {
                putchar(meta[m]);
            }
        }
        putchar(buffer[i]);
    } // End for all the characters.
} // End print_annotated_buffer()

/* ====================================================
// After dismissing mPRE and mCOM!
    if (NLRI > 0)                          // Indented.
       add_OBR(NLRI)
    else                                   // Not indented.
        if (LEW == mCOL || LEW == mSEM || LEW == mCBR)
            {} // PASS
        else if (LEW == mESC)
            remove_escape()
        else
            add_SEM()
        if (NLRI < 0)                      // Outdented.
            add_CBR(NLRI)
    ==================================================== */
void render_line(const char *buffer, const char *meta,size_t size,int S, int E, int spi, int tli, int nli) {
    int LEW; // Line Ends With. Last serious char position. Excluding include mTSP and mCOM.
    int NLRI; // Next Line Relative Indent. Levels, not spaces.
    int add_OBR= 0, add_CBR= 0, add_SEM= 0, rm_ESC= 0; // Action flags for this line.
    // Back scan the line for the LEW char's position.
    int b= E; while (b>=S && meta[b] != mC && meta[b] != mCHR && meta[b] != mSTR) b--; LEW= b;
    NLRI= spi?(nli - tli) / spi:0; // Only worry about indent levels if there is any indenting.
    if (NLRI > 0 && buffer[LEW] != '\\') {   // Next line is indented.
        add_OBR= 1;
    } else {          // Next line is not indented.
        if (buffer[LEW] == ':' || buffer[LEW] == ';') // || buffer[LEW] == '}')
            LEW=E+1; // Skip the LEW to leave it alone.
            //{} // PASS
        else if (meta[LEW] == mC && buffer[LEW] == '\\')
            rm_ESC= 1;
        else
            add_SEM= 1;
        if (NLRI < 0) // Next line is "outdented".
            add_CBR= 1;
    }
    for (int f= S; f<=E; f++) { // Loop to output this line's characters!
        if (f == LEW) {
            if (add_OBR) { putchar(buffer[f]); putchar(' '); putchar('{'); } 
            else if (rm_ESC) {}
            else if (add_SEM) {putchar(buffer[f]); putchar(';');}
        } else { // Not at Line Ends With important char.
            putchar(buffer[f]); // Just print it as supplied.
        }
    }
    if (add_CBR) {
        for (;NLRI<0;NLRI++) {
            for (int i= 0; i<tli-spi; i++) putchar(' '); // Indent for the closing brace.
            putchar('}'); // The actual closing brace.
            putchar('\n'); // This line must be ended like the sent line was.
            tli -= spi;
        }
    }
} // End render_line()

void generate_c(const char *buffer, size_t size, const char *meta, int spi) {
    int S= 0; // Line's start position.
    int nli= 0; // Next line's indent of mLSP. 0 if this is last line.
    int tli= 0; // This line's indent of mLSP.
    int realEOL= 1; // Flag that an escape is not happening and EOLs are good.
    while (meta[tli] == mLSP) tli++; // Prime with first line's indent.
    for (int c=0; c<size; c++){ // Break out lines from whole file.
        if (buffer[c] == '\\' && meta[c] == mC) { realEOL= 0; }
        else if (meta[c] == mEOL && realEOL) {
            for (int c2= c+1; c2<size; c2++) { // Look ahead at next indent.
                if (meta[c2] == mLSP) {
                    nli++; // Next line indent underway.
                } else if (meta[c2] == mEOL) { 
                    nli= 0;
                } else if (meta[c2] == mCOM || meta[c2] == mPRE) { 
                // More (blank) lines? Or other nugatory characters? Pass.
                } else {
                    break; // Next line indent finished or there was not a next line.
                }
            } // End look ahead for next line's indent.
            render_line(buffer,meta,size,S,c,spi,tli,nli);
            S= c+1; tli= nli; nli= 0; // Reset for next line.
        } // End else if - found an EOL.
        else if (meta[c] == mEOL && !realEOL) {
            render_line(buffer,meta,size,S,c,spi,tli,tli);
            realEOL= 1; // Go back to assuming that EOLs are real.
            S= c+1; nli= 0; // Reset for next line.
        }
    } // End for c first pass through file.
} // End generate_c()

int main(int argc, char *argv[]) {
    const ConfigPack conf= parse_options(argc, argv);
    if (argc-optind < 1) {
        usage(argv[0]);
        exit(1);
    }
    FILE *file= fopen(argv[optind], "r");
    if (!file) {
        perror("Error opening file");
        return 1;
    }
    fseek(file, 0, SEEK_END);   // Put File Position Indicator at end.
    long file_size= ftell(file); // With FPI at end, effectively reports file size.
    fseek(file, 0, SEEK_SET);   // Set FPI back to the start.
    char *buffer= malloc(file_size + 1); // +1 to make room for a null terminator.
    char *meta= malloc(file_size + 1);
    if (!buffer || !meta) {
        perror("Memory allocation failed");
        fclose(file);
        free(buffer); free(meta);
        return 1;
    }
    size_t size= fread(buffer, 1, file_size, file);
    buffer[size]= '\0'; // Not strictly necessary but allows buffer to act as string.
    fclose(file);
    memset(meta, 0, size + 1); // Zero out meta map. +1 is for null byte added.
    map_major_parts(buffer, size, meta);
    map_leading_whitespace(buffer, size, meta);
    map_trailing_whitespace(buffer, size, meta);
    if (conf.map) {
        print_annotated_buffer(buffer, size, meta);
    } else {
        int spi= calc_spaces_per_indent(buffer,size,meta);
        //printf("Spaces/indent:%d\n",spi);
        generate_c(buffer,size,meta,spi);
    }
    free(buffer); free(meta);
    return 0;
} // End main()
