/*
===================================================================================
pmb2ap.c

  Copyright (C) 2017 Ken Pettit. All rights reserved.
  Author: Ken Pettit <pettitkd@gmail.com>

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in
   the documentation and/or other materials provided with the
   distribution.
3. Neither the name pbm2ap nor the names of its contributors may be
   used to endorse or promote products derived from this software
   without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

===================================================================================
*/

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

/*
=====================================================
Structure for managing long sequence detection and
replacement.
=====================================================
*/
typedef struct longseq_s
{
    unsigned char ch[100];
    int           seqlen;
    int           count;
} longseq_t;
longseq_t longseq[1000];
int       longseq_count = 0;

/*
=====================================================
Structure for managing tuple sequence detection and
replacement.
=====================================================
*/
typedef struct tuple_s
{
    unsigned char ch1;
    unsigned char ch2;
    unsigned char ch3;
    int           count;
} tuple_t;

tuple_t tuple[1000];
int     tuple_count = 0;

int     verbose = 0;
int     silent = 0;

/*
===================================================================================
Find and encode tuples and long sequences in the 
generated AP string.
===================================================================================
*/
void dual_encode(char *data, int maxlen)
{
    int             x, n;
    int             y, z, i, datastart;
    int             swapped, replaced;
    int             first = 1;
    unsigned char   fmt;
    unsigned char   outstr[32768], c;

    // Copy original data to temp string
    strcpy((char *) outstr, data);
    tuple_count = 0;
    longseq_count = 0;

    // Search for a repitition of 3 or more bytes
    for (x = 0; x < sizeof(tuple) / sizeof(tuple_t); x++)
    {
        tuple[x].count = 0;
        longseq[x].count = 0;
    }
    n = strlen((char *) outstr);
    for (x = 0; x < n-9; x++)
    {
        c = outstr[x];

        // Test if this sequence already counted
        for (z = 0; z < tuple_count; z++)
        {
            if (c == tuple[z].ch1 && outstr[x+1] == tuple[z].ch2)
            {
                // Sequence already counted
                x += 1;
                break;
            }
        } 
        if (tuple[z].count != 0)
            continue;

        // Test for dual 
        for (y = x + 2; y < n; y++)
        {
            // Test if first byte matches
            if (outstr[y] == c)
            {
                // Now test 2nd and 3rd bytes
                if (outstr[x+1] == outstr[y+1])
                {
                    // Three byte repeat
                    tuple[tuple_count].ch1 = c;
                    tuple[tuple_count].ch2 = outstr[x+1];
                    tuple[tuple_count].count++;
                    
                    // Advance y in case it can double match
                    y += 1;
                }
            }
        }

        // Advance tuple_count if a match found
        if (tuple[tuple_count].count > 0)
        {
            tuple_count++;
        //    if (tuple_count == 6+0x1A)
        //        tuple_count++;
            x += 1;
        }
    }

    // Sort the tuples by count
    do
    {
        tuple_t temp;
        swapped = 0;
        for (x = 0; x < tuple_count - 1; x++)
        {
            if (tuple[x+1].count > tuple[x].count)
            {
                temp = tuple[x];
                tuple[x] = tuple[x+1];
                tuple[x+1] = temp;
                swapped = 1;
            }
        }
    } while (swapped);
    
    if (tuple_count > 25)
        tuple_count = 25;
    for (y = 0; y < tuple_count; y++)
    {
        if (tuple[y].count <= 1)
        {
            tuple_count = y;
            break;
        }
        if (verbose)
        printf("Two: %02X %02X %d\n", tuple[y].ch1,
                tuple[y].ch2, tuple[y].count);
    }

    // Put original header info into output string
    x = 0;
    i = 0;
    while (data[x] >= '9')
    {
        data[i++] = outstr[x++];
    }
    fmt = data[i];
    data[i++] = outstr[x++];
    switch (fmt)
    {
        case '1':
        case '2':
            break;
        case '3':
            data[i++] = outstr[x++];
            data[i++] = outstr[x++];
            break;
        case '4':
            data[i++] = outstr[x++];
            data[i++] = outstr[x++];
            data[i++] = outstr[x++];
            data[i++] = outstr[x++];
            break;
    }

    // Add the tuple count
    data[i++] = tuple_count + 1;

    // Add the tuples
    for (y = 0; y < tuple_count; y++)
    {
        //if (tuple[y].count > 1)
        {
            data[i++] = tuple[y].ch1;
            data[i++] = tuple[y].ch2;
        }
    }
    datastart = i;

    // Now scan the original string and swap out tuples
    for (; x < n; x++)
    {
        // Test for a tuple
        if (x < n-2)
        {
            // Search all tuples for a match
            replaced = 0;
            for (z = 0; z < tuple_count; z++)
            {
                if (outstr[x] == tuple[z].ch1 && outstr[x+1] == tuple[z].ch2)
                {
                    // Don't use 0x1A (EOF)
                    //if (z+6 == 0x1A)
                        //break;

                    if (first)
                        data[i++] = ' ';
                    // Tuple match found.  Replace 
                    data[i++] = z+6;
                    x += 1;
                    replaced = 1;
                    break;
                }
            }

            if (!replaced)
                data[i++] = outstr[x];
        }
        else
            // Copy data directly
            data[i++] = outstr[x];

        first = 0;
    }
    data[i] = 0;

    // Now look for long sequences
    strcpy((char *) outstr, data);
    n = strlen((char *) outstr);

    // Skip the format string and dual table
    for (x = datastart; x < n-9; x++)
    {
        int seqlen = 100;
        int s;

        c = outstr[x];
        if (!c)
            continue;

        // Test if this sequence already counted
        for (z = 0; z < longseq_count; z++)
        {
            // Test if x is part of this sequence
            for (y = 0; y < longseq[z].seqlen; y++)
                if (outstr[x+y] != longseq[z].ch[y])
            {
                // Sequence already counted
                break;
            }
            if (y == longseq[z].seqlen)
            {
                x += longseq[z].seqlen;
                break;
            }
        } 
        if (longseq[z].count != 0)
            continue;

        // Test for long sequence
        for (y = x + 6; y < n; y++)
        {
            if (c == outstr[y])
            {
                for (s = 1; s < seqlen && x+s < y; s++)
                {
                    if (outstr[x+s] != outstr[y+s] || outstr[x+s] == 0)
                        break;
                }
                if (s > 5)
                {
                    seqlen = s;
                    longseq[longseq_count].count++;
                    longseq[longseq_count].seqlen = s;
                    if (verbose)
                        printf("   %04X  %04X\n", x, y);
                    for (z = 0; z < s; z++)
                        outstr[y+z] = 0;
                    y += s-1;
                }    
            }
        }
        if (longseq[longseq_count].count)
        {
            if (verbose)
                printf("Long:  ");
            for (i = 0; i < longseq[longseq_count].seqlen; i++)
            {
                longseq[longseq_count].ch[i] = outstr[x+i];
                if (verbose)
                    printf("%02X ", outstr[x+i]);
            }
            if (verbose)
                printf(":  %d\n", longseq[longseq_count].count++);
            x += longseq[longseq_count++].seqlen;
            continue;
        }
    }

    // Restore the string
    strcpy((char *) outstr, data);
    if (longseq_count == 0)
        return;

    // Sort the longseq by count
    do
    {
        longseq_t temp;
        swapped = 0;
        for (x = 0; x < longseq_count - 1; x++)
        {
            if (longseq[x+1].count > longseq[x].count)
            {
                temp = longseq[x];
                longseq[x] = longseq[x+1];
                longseq[x+1] = temp;
                swapped = 1;
            }
        }
    } while (swapped);
    if (longseq_count > 5)
        longseq_count = 5;
    
    // Now perform long sequence replacement
    i = datastart;
    x = datastart;
    for (z = 0; z < longseq_count; z++)
    {
        if (longseq[z].count > 1)
        {
            if (verbose)
                printf("Long: %d  ", longseq[z].count);
            data[i++] = longseq[z].seqlen;
            for (y = 0; y < longseq[z].seqlen; y++)
            {
                data[i++] = longseq[z].ch[y];
                if (verbose)
                    printf("%02X ", longseq[z].ch[y]);
            }
            if (verbose)
                printf("\n");
        }
    }

    first = 1;
    for (;x < n; x++)
    {
        // Test for long sequence match
        for (z = 0; z < longseq_count; z++)
        {
            if (longseq[z].count == 1)
                continue;

            for (y = 0; y < longseq[z].seqlen; y++)
            {
                if (outstr[x+y] != longseq[z].ch[y])
                    break;
            }

            // Test for match
            if (y == longseq[z].seqlen)
                break;
        }

        if (z == longseq_count)
        {
            if (first && outstr[x] < ' ')
                data[i++] = ' ';
            data[i++] = outstr[x];
        }
        else
        {
            if (first)
                data[i++] = ' ';
            // Perform replacement
            data[i++] = z+1;
            x += longseq[z].seqlen-1;
            if (verbose)
                printf("Long replace: %d\n", z);
        }
        first = 0;
    }
    data[i] = 0;
}

/*
===================================================================================
Convert PBM image file to an AsciiPixels format file.
===================================================================================
*/
int conv_asciipix(FILE* out, char* data, char* end, int x, int y, int outx, 
        int outy, int allow7, int largerun, int rom_opt, int upper_opt, int oldschool, 
        char *mask_data, char *mask_endptr)
{
    int             n, i, r, c, m;
    int             count;
    unsigned char   mask, byte;
    unsigned char   *ptr = (unsigned char *) data;
    unsigned char   *mptr = (unsigned char *) mask_data;
    unsigned char   outstr[32768];
    unsigned char   opt[32768];
    unsigned char   cmodestr[2][32768];
    unsigned char   pixels[240*128];
    unsigned char   mask_pixels[240*128+6];
    unsigned char   *pptr;
    unsigned char   *mpptr;
    unsigned char   *endptr;
    unsigned char   temp[128];
    unsigned char   currmask;
    int             paircount[256];
    int             skip = 0;
    int             run = 6;
    int             rpt;
    char            cmode = 0;
    int             cmodesize[2];
        
    if (allow7)
        run = 7;
    if (oldschool)
        run = 6;
    cmodesize[0] = 0;
    cmodesize[1] = 0;

    // Oldschool format does not detect "mostly black" or "mostly white"
    // images and make optimizations based on that.
    if (oldschool)
    {
        if (outx == 240 && outy == 64)
            sprintf((char *) outstr, "AP1");
        else if (outx == 240 && outy == 128)
            sprintf((char *) outstr, "AP2");
        else if ((outx <= 'z'-35 && outy <= 'z'-35) || (rom_opt && (outx + 35 < 256)))
            sprintf((char *) outstr, "AP3%c%c", outx+35, outy+35);
        else
            sprintf((char *) outstr, "AP4%c%c%c%c", (int)(outx/80)+35, (int)(outx%80)+35, 
                    (int) (outy/80)+35, (int)(outy%80)+35);
    }
    else
    {
        if (outx == 240 && outy == 64)
            sprintf((char *) outstr, "APc1");
        else if (outx == 240 && outy == 128)
            sprintf((char *) outstr, "APc2");
        else if ((outx <= 'z'-35 && outy <= 'z'-35) || (rom_opt && (outx + 35 < 256)))
            sprintf((char *) outstr, "APc3%c%c", outx+35, outy+35);
        else
            sprintf((char *) outstr, "APc4%c%c%c%c", (int)(outx/80)+35, (int)(outx%80)+35, 
                    (int) (outy/80)+35, (int)(outy%80)+35);
    }

    /* First PBM convert pixel data into individual bytes of 0 or 1 */
    pptr = pixels;
    mpptr = mask_pixels;
    if (x < outx)
    {
        skip = (outx/8 - (outx+7)/8) * 8;
        pptr += skip/2;
    }

    // Loop across height and width
    for (r = 0; r < y; r++)
    {
        for (c = 0; c < x; c+= 8, ptr++)
        {
            int  pix = 0;
            mask = 0x80;
            while (mask && c+pix < x)
            {
                if (mask & *ptr)
                    *pptr++ = 1;
                else
                    *pptr++ = 0;

                // Convert the mask also if present
                if (mptr)
                {
                    if (mask & *mptr)
                        *mpptr++ = 1;
                    else
                        *mpptr++ = 0;
                }
                else
                    *mpptr++ = 0;
                mask >>= 1;
                pix++;
            }
        }
        pptr += skip;
        mpptr += skip;

        if (mptr)
            mptr++;
    }
    *mpptr = *(mpptr-1);
    *mpptr = *(mpptr-1);
    *mpptr = *(mpptr-1);
    *mpptr = *(mpptr-1);
    *mpptr = *(mpptr-1);

    /* Now process the data */
    ptr = pixels;
    mptr = mask_pixels;
    n = 0;
    currmask = 0;
    count = outx * outy;
    endptr = ptr + count;
    (void) endptr;
    for (x = 0; x < count;)
    {
        /* Get pixel color of next pixel */
        c = *ptr;
        m = *mptr;

        /* Test for a mask change */
        if (rom_opt && currmask != m)
        {
            /* Emit a mask toggle code */
            strcat((char *) outstr, "\"");
            currmask = m;
        }

        /* Check for repeated pixel color */
        i = 1;
        r = 1;
        while (ptr[i] == c && mptr[i] == m && x+i < count)
        {
            i++;
            r++;
        }

        /* If we have more than 6 pixels in a row that are the same color,
           then encode them using special encoding.
         */
        if (r > run)
        {
            int  r1 = r;

            /* The largest pixel run we can prepresent with ASCII80 is 6400 */
            while (r1 >= 6400)
            {
                if (verbose)
                    printf("R=%d\n", r1);

                /* Use either y or z based on 1/0 pixel */
                if (c)
                    temp[0] = 'z';
                else
                    temp[0] = 'y';
                temp[1] = '#' + 6399 / 80;
                temp[2] = '#' + 6399 % 80;
                temp[3] = 0;
                strcat((char *) outstr, (char *) temp);
                n += 3;
                r1 -= 6399;
            }

            // If we have more than 96 pixel run, then use a 
            // three byte encoding.
            if (r1 >= 16 + 80)
            {
                /* Use either y or z based on 1/0 pixel */
                /* Use either w or x based on 1/0 pixel */
                if (c)
                    temp[0] = 'z';
                else
                    temp[0] = 'y';
                temp[1] = '#' + r1 / 80;
                temp[2] = '#' + r1 % 80;
                temp[3] = 0;
                strcat((char *) outstr, (char *) temp);
                n += 3;
            }

            // Else if we have 17-96 pixels, use 2 byte encoding
            else if (r1 > 16)
            {
                /* Use either w or x based on 1/0 pixel */
                if (c)
                    temp[0] = 'x';
                else
                    temp[0] = 'w';
                temp[1] = r1 - 17 + '#';
                temp[2] = 0;
                strcat((char *) outstr, (char *) temp);
                n += 2;
            }

            // Else use one byte encoding
            else
            {
                temp[0] = '#' + 64 + r1-7;
                if (c)
                    temp[0] += 10;
                temp[1] = 0;
                strcat((char *) outstr, (char *) temp);
                n++;
            }
            x += r;
            ptr += r;
            mptr += r;
        }
        else
        {
            /* Test if less than 6 pixels of the same mask type */
            if (mptr[1] != m || mptr[2] != m || mptr[3] != m || mptr[4] != m || mptr[5] != m)
            {
                // Fewer than 6 pixels of this mask.  Emit individual 0/1 pixels
                strcat((char *) outstr, "\"");
                for (r = 0; mptr[r] == m; r++)
                    if (ptr[r])
                        strcat((char *) outstr, "{");
                    else
                        strcat((char *) outstr, "}");
                ptr += r;
                mptr += r;
                x += r;
                currmask = *mptr;
                continue;
            }

            /* Test for special 01010101 or 10101010 pattern */
            if (ptr[2] == c && ptr [4] == c && ptr[6] == c &&
                ptr[1] != c && ptr[3] != c && ptr[5] != c && ptr[7] != c &&
                mptr[1] == m && mptr[2] == m && mptr[3] == m && mptr[4] == m &&
                mptr[5] == m && mptr[6] == m && mptr[7] == m)
            {
                if (c == 0)
                {
                    strcat((char *) outstr, "{");
                }
                else
                {
                    strcat((char *) outstr, "}");
                }
                ptr += 8;
                mptr += 8;
                x += 8;
                n++;
            }
            else
            {
                int len = strlen((char *) outstr);

                /* Encode 6 bits into an ASCII character */
                mask = 1;
                byte = 0;
                for (i = 0; i < run; i++)
                {
                    if (ptr[i])
                        byte |= mask;
                    mask <<= 1;
                }
                if (run == 6 || !(rom_opt || upper_opt))
                {
                    unsigned char valtest = byte;
                    int lc, allzero;
                    if (ptr[i])
                        valtest |= mask;

                    // Test if bit after next is a '0'
                    allzero = 0;
                    for (lc = 9; lc <= largerun; lc++)
                        allzero |= ptr[i+1+lc-8] | (mptr[i+1+lc-8] != m);

                    if ((rom_opt || upper_opt) && (ptr[i+1] == 0 && 
                         mptr[i+1] == m && allzero == 0 && valtest < 96))
                    {
                        temp[0] = valtest | 0x80;
                        i += 2 + (largerun-8);
                        x += 2 + (largerun-8);
                    }
                    else
                        temp[0] = byte + '#';
                }
                else
                    temp[0] = byte | 0x80;
                temp[1] = 0;
                outstr[len] = temp[0];
                outstr[len+1] = 0;
                //strcat((char *) outstr, (char *) temp);
                n++;
                ptr += i;
                x += run;
            }
        } 
    }

    /* Now find repeated characters.  For 5 in a row, we use ~c and for 3 in a row !c */
    for (cmode = 0; cmode < 2; cmode++)
    {
        n = strlen((char *) outstr);
        for (i = 0; i < 256; i++)
            paircount[i] = 0;
        r = 4-oldschool;
        i = 4-oldschool;
        count = 4-oldschool;
        strncpy((char *) opt, (char *) outstr, 4-oldschool);
        for (x = 4; x < n; )
        {
            /* Skip over any 'w', 'x', 'y' or 'z' multi-character sequence */
            if (outstr[x] == 'w' || outstr[x] == 'x')
            {
                if (upper_opt & ((outstr[x] == (cmode ? 'x' : 'w')) && (outstr[x+1] < (31 + '#'))))
                {
                    opt[i++] = (outstr[x+1] - '#') | 0xE0;
                    x+=2;
                    r++;
                }
                else
                {
                    /* Copy directly to opt array */
                    opt[i++] = outstr[x++];
                    opt[i++] = outstr[x++];
                    r += 2;
                }
                continue;
            }
            else if (outstr[x] == 'y' || outstr[x] == 'z')
            {
                if (!oldschool & (outstr[x] == (cmode ? 'z' : 'y') && outstr[x+1] == '$'))
                {
                    opt[i++] = '|';
                    x += 2;
                }
                else
                {
                    /* Copy directly to opt array */
                    opt[i++] = outstr[x++];
                    opt[i++] = outstr[x++];
                }
                opt[i++] = outstr[x++];
                r += 3;
                continue;
            }

            /* Get the current char value */
            c = outstr[x];

            /* Do pair count statistics */
            if (outstr[x+1] == c && outstr[x+2] != c && outstr[x-1] != c)
                paircount[c]++;

            /* Search for repeated pattern */
            if (upper_opt)
            {
                for (rpt = 1; rpt+x < n && outstr[x+rpt] == c; rpt++)
                    ;
                if (rpt > 5 && (rpt < 255 - '"'))
                {
                    opt[i++] = 0x7F;
                    opt[i++] = rpt-1;
                    opt[i++] = c;
                    r += rpt;
                    x += rpt;
                    continue;
                }
            }

            /* Search for 5 chars in a row */

            if (x+5 < n)
            {
                if (outstr[x+1] == c && outstr[x+2] == c && outstr[x+3] == c &&
                    outstr[x+4] == c)
                {
                    /* Use the special ~ character */
                    opt[i++] = '~';
                    opt[i++] = c;
                    x += 5;
                    r += 2;
                    continue;
                }
            }

            if (x+3 < n)
            {

                /* Not a 5 character sequence.  Try a 3 character sequence */
                if (outstr[x+1] == c && outstr[x+2] == c)
                {
                    /* Use the special ~ character */
                    opt[i++] = '!';
                    opt[i++] = c;
                    x += 3;
                    r += 2;
                    continue;
                }
            }

            /* Just copy the input to the output */
            opt[i++] = c;
            x++;
            r++;
        }
        opt[i] = 0;
        cmodesize[(int)cmode] = strlen((char *) opt);
        strcpy((char *) cmodestr[(int)cmode], (char *) opt);
        if (oldschool)
            break;
    }

    // Choose the smallest encoding
    if (!oldschool)
    {
        if (cmodesize[0] < cmodesize[1])
        {
            strcpy((char *) opt, (char *) cmodestr[0]);
            opt[2] = 'c' + ((largerun - 8) << 1);
        }
        else
            opt[2] = 'b' + ((largerun - 8) << 1);
    }

    // Search for a repitition of 3 or more bytes
    if (rom_opt)
    {
        dual_encode((char *) opt, sizeof(opt));
    }

    // Print to output file
    fprintf(out, "%s", opt);
    return strlen((char *) opt);
}

/*
===================================================================================
Create an 8085 ASM program containing the AsciiPixels data.
===================================================================================
*/
void generate_asm_output(FILE* in, char * filename)
{
    char            asmfile[512];
    char            name[128];
    unsigned char   data[32768];
    int             len, x, col;
    FILE*           out;

    strcpy((char *) asmfile, filename);
    len = strlen((char *) asmfile);
    while (asmfile[len-1] != '.')
        len--;
    asmfile[len-1] = '\0';
    strcpy((char *) name, (char *) asmfile);
    strcpy(&asmfile[len-1], ".asm");
    if (!silent)
        printf("ASM FILE: %s\n", asmfile);

    // Try to create the ASM file
    if ((out = fopen(asmfile, "w")) == NULL)
    {
        printf("Unable to create %s\n", asmfile);
        return;
    } 

    // Convert name toupper
    len = strlen((char *) name);
    for (x = 0; x < len; x++)
        name[x] = toupper(name[x]);

    // Start at beginning of input file and read all data
    fseek(in, 0, SEEK_SET);
    len = fread(data, 1, sizeof(data), in);

    // Write a header
    fprintf(out, "; ===============================================================================\n");
    fprintf(out, "; Ascii Pixel Image file generated by pbm2ap from %s\n", filename);
    fprintf(out, ";    image size = %d\n", len);
    fprintf(out, "; ===============================================================================\n");
    fprintf(out, "API_%s:\n", name);
    fprintf(out, "\tdb   \"");
    for (x = 0; x < len && data[x] > '9'; x++)
        fprintf(out, "%c", data[x]);
    fprintf(out, "%c\"", data[x]);
    if (data[x] == '3')
    {
        // Print 2 single byte coords
        fprintf(out, ", %d, ", data[++x]);
        fprintf(out, "%d", data[++x]);
    }
    else if (data[x] == '4')
    {
        // Print 2 2-byte coords
        fprintf(out, ", %d, ", data[++x]);
        fprintf(out, "%d, ", data[++x]);
        fprintf(out, "%d, ", data[++x]);
        fprintf(out, "%d", data[++x]);
    }
    fprintf(out, "\n");
    x++;

    // Now output all data
    col = 0;
    for (; x < len; x++)
    {
        // Print new db line
        if (col == 0)
        {
            fprintf(out, "\tdb   ");
        }
        if (col != 0)
            fprintf(out, ", ");

        fprintf(out, "0x%02X", data[x]);
        col++;
        if (col == 16)
        {
            fprintf(out, "\n");
            col = 0;
        }
    }
    if (col != 0)
        fprintf(out, "\n");
    fprintf(out, "\n");

    // Close the output file
    fclose(out);
}

/*
===================================================================================
Open and read the PBM file, parse the header and determine the image geometry.  
Also return the pointer to the first byte of data.
===================================================================================
*/
FILE* open_pbm_file(char *filename, long *x, long* y, char** dataptr, char** endptr)
{
    FILE*   in;
    char *  data, *ptr;
    int     len;

    if ((in = fopen(filename, "rb")) == NULL)
    {
        printf("unable to open %s\n", filename);
        return NULL;
    }

    /* Read the input data */
    fseek(in, 0, SEEK_END);
    len=ftell(in);
    fseek(in, 0, SEEK_SET);
    data = malloc(len);
    fread(data, 1, len, in);
    fclose(in);

    /* Validate file format */
    if (data[0] != 'P' || data[1] != '4')
    {
        printf("Invalid input format\n");
        fclose(in);
        return NULL;
    }

    /* Test for file comment */
    ptr = &data[3];
    if (*ptr == '#')
    {
        while (*ptr != '\n')
            ptr++;
        ptr++;
    }

    /* Read the x dimension */
    *x = atoi(ptr);
    while (*ptr != ' ' && *ptr != '\t' && *ptr != '\n')
        ptr++;
    while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n')
        ptr++;

    /* Read the y dimension */
    *y = atoi(ptr);
    while (*ptr != ' ' && *ptr != '\t' && *ptr != '\n')
        ptr++;

    /* Skip the single space after the y dimension */
    ptr++;

    *dataptr = ptr;
    *endptr = ptr + *y*(*x+7)/8;

    return in;
}

/*
===================================================================================
Main program entry point.
===================================================================================
*/
int main(int argc, char* argv[])
{
    FILE*   in;
    FILE*   out;
    FILE*   maskfd;
    long    x, y, outx, outy;
    char *  maskfile = NULL;
    char    *data_ptr, *data_endptr;
    int     rom_opt = 0, upper_opt = 0, oldschool = 0;
    int     run, minsize, minrun;
    int     opt;
    int     size[5];

    char    *mask_ptr, *mask_endptr;
    long    mask_x, mask_y;

    while ((opt = getopt(argc, argv, "m:orsuv")) != -1) 
    {
         switch (opt) {
         case 'o':
             oldschool = 1;
             break;
         case 'r':
             rom_opt = 1;
             break;
         case 'u':
             upper_opt = 1;
             break;
         case 'm':
             maskfile = optarg;
             break;
         case 'v':
             verbose = 1;
             break;
         case 's':
             silent = 1;
             break;

         default: /* '?' */
             fprintf(stderr, "Usage: %s [-m maskfile [-r] [-u] infile outfile\n",
                     argv[0]);
             exit(EXIT_FAILURE);
         }
    }

    // Print copyright info
    if (!silent)
    {
        printf("\nPBM to AsciiPixels Converter\n");
        printf("Version 1.0\n");
        printf("Copyright 2017, Ken Pettit\n");
        printf("BSD 3-clause license\n");
        printf("\n");
    }


     if (optind+1 >= argc) 
     {
         fprintf(stderr, "Usage: %s [-rsuv] infile outfile\n", argv[0]);
         fprintf(stderr, "\n");
         fprintf(stderr, "       -r   Encode for OptROM.  Uses ALL bytes 0-FFh\n");
         fprintf(stderr, "               Not suitable for use with BASIC\n\n");
         fprintf(stderr, "       -s   Perform silent conversion\n\n");
         fprintf(stderr, "       -u   Encode using upper ASCII bytes 80h-FFh also\n\n");
         fprintf(stderr, "       -v   Be verbose during the conversion\n\n");
         exit(EXIT_FAILURE);
     }

    // Open the input file
    if ((in = open_pbm_file(argv[optind], &x, &y, &data_ptr, &data_endptr)) == NULL)
    {
        return 1;
    }

    // If a mask file given, try to open it
    if (maskfile)
    {
        maskfd = open_pbm_file(maskfile, &mask_x, &mask_y, &mask_ptr, &mask_endptr);
        
        // Validate we can open the maskfile
        if (maskfd == NULL)
        {
            fprintf(stderr, "Unable to open mask file %s\n", maskfile);
            return 1;
        }

        // Validate coords are the same
        if (mask_x != x || mask_y != y)
        {
            fprintf(stderr, "Mask size (%ld, %ld) does not match image size (%ld, %ld)\n",
                    mask_x, mask_y, x, y);
            return 1;
        }
    }

    if (!silent)
        printf("Image size: X=%ld, Y=%ld\n", x, y);

    // Try different large run values and find the run with the
    // smallest resulting output file.
    outx = x;
    outy = y;
    if (rom_opt || upper_opt)
    {
        // We support large runs from 8 - 13 pixels
        for (run = 8; run < 13; run++)
        {
            // Open (re-open) the file for this run
            if ((out = fopen(argv[optind + 1], "wb+")) == NULL)
            {
                printf("unable to open %s\n", argv[optind + 1]);
                fclose(in);
                return 1;
            }

            size[run-8] = conv_asciipix(out, data_ptr, data_endptr, x, y, outx, outy, 0, run, rom_opt, upper_opt,
                    oldschool, mask_ptr, mask_endptr);
            if (verbose)
                printf("Run: %d   Size = %d\n", run, size[run-8]);

            // Close the file.  We will reopen as needed
            fclose(out);
        }
        
        minsize = 999999;
        for (run = 8; run < 13; run++)
        {
            if (size[run-8] < minsize)
            {
                minrun = run;
                minsize = size[run-8];
            }
        }
    }
    else
    {
        minrun = 8;
    }
    
    minrun = 10;
    // Now create the file using the minimum size
    out = fopen(argv[optind + 1], "wb+");
    conv_asciipix(out, data_ptr, data_endptr, x, y, outx, outy, 0, minrun, rom_opt, upper_opt,
            oldschool, mask_ptr, mask_endptr);

    // If this is a ROM option conversion, then generate assembly code
    if (rom_opt)
    {
        // When OptROM selected, generate an ASM output file
        generate_asm_output(out, argv[optind + 1]);
    }
    fclose(out);

    return 0;
}

