/*
 * Copyright 2002-2010 Guillaume Cottenceau.
 * Copyright 2015-2018 Paul Gardner-Stephen.
 *
 * This software may be freely redistributed under the terms
 * of the X11 license.
 *
 */

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

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <stdarg.h>

#define PNG_DEBUG 3
#include <png.h>

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

char *vhdl_prefix=
  "library IEEE;\n"
  "use IEEE.STD_LOGIC_1164.ALL;\n"
  "use ieee.numeric_std.all;\n"
  "use work.debugtools.all;\n"
  "\n"
  "--\n"
  "entity charrom is\n"
  "port (Clk : in std_logic;\n"
  "        address : in integer range 0 to 4095;\n"
  "        -- chip select, active low       \n"
  "        cs : in std_logic;\n"
  "        data_o : out std_logic_vector(7 downto 0);\n"
  "\n"
  "        writeclk : in std_logic;\n"
  "        -- Yes, we do have a write enable, because we allow modification of ROMs\n"
  "        -- in the running machine, unless purposely disabled.  This gives us\n"
  "        -- something like the WOM that the Amiga had.\n"
  "        writecs : in std_logic;\n"
  "        we : in std_logic;\n"
  "        writeaddress : in unsigned(11 downto 0);\n"
  "        data_i : in std_logic_vector(7 downto 0)\n"
  "      );\n"
  "end charrom;\n"
  "\n"
  "architecture Behavioral of charrom is\n"
  "\n"
  "-- 4K x 8bit pre-initialised RAM for character ROM\n"
  "\n"
  "type ram_t is array (0 to 4095) of std_logic_vector(7 downto 0);\n"
  "signal ram : ram_t := (\n"
  "\n";

char *vhdl_suffix=
  ");\n"
  "\n"
  "begin\n"
  "\n"
  "--process for read and write operation.\n"
  "PROCESS(Clk,ram,writeclk)\n"
  "BEGIN\n"
  "\n"
  "  if(rising_edge(writeClk)) then \n"
  "    if writecs='1' then\n"
  "      if(we='1') then\n"
  "        ram(to_integer(writeaddress)) <= data_i;\n"
  "      end if;\n"
  "    end if;\n"
  "    data_o <= ram(address);\n"
  "  end if;\n"
  "END PROCESS;\n"
  "\n"
  "end Behavioral;\n";

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

int x, y;

int width, height;
png_byte color_type;
png_byte bit_depth;

png_structp png_ptr;
png_infop info_ptr;
int number_of_passes;
png_bytep * row_pointers;

FILE *infile;
FILE *outfile;

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

void abort_(const char * s, ...)
{
  va_list args;
  va_start(args, s);
  vfprintf(stderr, s, args);
  fprintf(stderr, "\n");
  va_end(args);
  abort();
}

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

void read_png_file(char* file_name)
{
  unsigned char header[8];    // 8 is the maximum size that can be checked
  size_t readResult;

  /* open file and test for it being a png */
  infile = fopen(file_name, "rb");
  if (infile == NULL)
    abort_("[read_png_file] File %s could not be opened for reading", file_name);

  readResult = fread(header, 1, 8, infile);
  if (readResult != 8)
    abort_("[read_png_file] File %s could not be read", file_name);
  if (png_sig_cmp(header, 0, 8))
    abort_("[read_png_file] File %s is not recognized as a PNG file", file_name);

  /* initialize stuff */
  png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

  if (!png_ptr)
    abort_("[read_png_file] png_create_read_struct failed");

  info_ptr = png_create_info_struct(png_ptr);
  if (!info_ptr)
    abort_("[read_png_file] png_create_info_struct failed");

  if (setjmp(png_jmpbuf(png_ptr)))
    abort_("[read_png_file] Error during init_io");

  png_init_io(png_ptr, infile);
  png_set_sig_bytes(png_ptr, 8);

  // Convert palette to RGB values
  png_set_expand(png_ptr);

  png_read_info(png_ptr, info_ptr);

  width = png_get_image_width(png_ptr, info_ptr);
  height = png_get_image_height(png_ptr, info_ptr);
  color_type = png_get_color_type(png_ptr, info_ptr);
  bit_depth = png_get_bit_depth(png_ptr, info_ptr);

  printf("Input-file is: width=%d, height=%d.\n", width, height);

  number_of_passes = png_set_interlace_handling(png_ptr);
  png_read_update_info(png_ptr, info_ptr);

  /* read file */
  if (setjmp(png_jmpbuf(png_ptr)))
    abort_("[read_png_file] Error during read_image");

  row_pointers = (png_bytep*) malloc(sizeof(png_bytep) * height);
  for (y=0; y<height; y++)
    row_pointers[y] = (png_byte*) malloc(png_get_rowbytes(png_ptr,info_ptr));

  png_read_image(png_ptr, row_pointers);

  if (infile != NULL) {
    fclose(infile);
    infile = NULL;
  }

  printf("Input-file is read and now closed\n");
}

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

struct rgb {
  int r;
  int g;
  int b;
};

struct rgb palette[2560];
int palette_first=16;
int palette_index=16; // only use upper half of palette

int palette_lookup(int r,int g, int b)
{
  int i;

  // Do we know this colour already?
  for(i=palette_first;i<palette_index;i++) {
    if (r==palette[i].r&&g==palette[i].g&&b==palette[i].b) {
      return i;
    }
  }
  
  // new colour
  if (palette_index>255) {
    fprintf(stderr,"Too many colours in image: Must be < 256, now up to %d\n",
	    palette_index);
  }
  if (palette_index>2559) exit(-1);
  
  // allocate it
  palette[palette_index].r=r;
  palette[palette_index].g=g;
  palette[palette_index].b=b;
  return palette_index++;
  
}

unsigned char nyblswap(unsigned char in)
{
  return ((in&0xf)<<4)+((in&0xf0)>>4);
}

void process_file(int mode, char *outputfilename)
{
  int multiplier=-1;
  if (png_get_color_type(png_ptr, info_ptr) == PNG_COLOR_TYPE_RGB)
    multiplier=3;

  if (png_get_color_type(png_ptr, info_ptr) == PNG_COLOR_TYPE_RGBA)
    multiplier=4;

  if (multiplier==-1) {
    fprintf(stderr,"Could not convert file to RGB or RGBA\n");
  }

  outfile=fopen(outputfilename,"w");
  if (outfile == NULL) {
    // could not open output file, so close all and exit
    if (infile != NULL) {
      fclose(infile);
      infile = NULL;
    }
    abort_("[process_file] File %s could not be opened for writing", outputfilename);
  }


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

  if (mode==0) {
    printf("mode=0 (logo)\n");
    // Logo mode

    // Pre-load in C64 palette, so that those colours can be re-used if required
    
    palette[0]=(struct rgb){.r=0,.g=0,.b=0};
    palette[1]=(struct rgb){.r=0xff,.g=0xff,.b=0xff};
    palette[2]=(struct rgb){.r=0xab,.g=0x31,.b=0x26};
    palette[3]=(struct rgb){.r=0x66,.g=0xda,.b=0xff};
    palette[4]=(struct rgb){.r=0xbb,.g=0x3f,.b=0xb8};
    palette[5]=(struct rgb){.r=0x55,.g=0xce,.b=0x58};
    palette[6]=(struct rgb){.r=0x1d,.g=0x0e,.b=0x97};
    palette[7]=(struct rgb){.r=0xea,.g=0xf5,.b=0x7c};
    palette[8]=(struct rgb){.r=0xb9,.g=0x74,.b=0x18};
    palette[9]=(struct rgb){.r=0x78,.g=0x73,.b=0x00};
    palette[10]=(struct rgb){.r=0xdd,.g=0x93,.b=0x87};
    palette[11]=(struct rgb){.r=0x5b,.g=0x5b,.b=0x5b};
    palette[12]=(struct rgb){.r=0x8b,.g=0x8b,.b=0x8b};
    palette[13]=(struct rgb){.r=0xb0,.g=0xf4,.b=0xac};
    palette[14]=(struct rgb){.r=0xaa,.g=0x9d,.b=0xef};
    palette[15]=(struct rgb){.r=0xb8,.g=0xb8,.b=0xb8};
    
    
    for (y=0; y<height; y++) {
      png_byte* row = row_pointers[y];
      for (x=0; x<width; x++) {
	png_byte* ptr = &(row[x*multiplier]);
	int r,g,b;
	if (ptr)
	  { r=ptr[0]; g=ptr[1]; b=ptr[2]; // a=ptr[3];
	  } else { r=0; g=0; b=0; }

	int c=palette_lookup(r,g,b);

	if (c>255) printf("Too many colours at (%d,%d)\n",x,y);
	
	/* work out where in logo file it must be written.
	   image is made of 8x8 blocks.  So every 8 pixels across increases address
	   by 64, and every 8 pixels down increases pixel count by (64*8), and every
	   single pixel down increases address by 8.
	*/
	int address=0;
	address+=0x300; // space for palettes
	address+=(x&7)+(y&7)*8;
	address+=(x>>3)*64;
	//	if (size==SIZE_LOGO)
	  address+=(y>>3)*64*(width/8);
	  //	else
	  //	  address+=(y>>3)*64*40;

	fseek(outfile,address,SEEK_SET);
	int n=fwrite(&c,1,1,outfile);
	if (n!=1) {
	  fprintf(stderr,"Could not write pixel (%d,%d) @ $%x\n",x,y,address);
          if (outfile != NULL) {
            fclose(outfile);
            outfile = NULL;
          }
	  exit(-1);
	}
      }
    }

    fprintf(stderr,"Writing out palette of %d values\n",palette_index-palette_first);
    for(int i=0;i<256;i++){
      int address;
      unsigned char c;
      int v;
      
      address=i+0x000;
      v=palette[i].r;
      c=(v>>4)|((v&0xf)<<4);
      fseek(outfile,address,SEEK_SET);
      fwrite(&c,1,1,outfile);
      
      address=i+0x100;
      v=palette[i].g;
      c=(v>>4)|((v&0xf)<<4);
      fseek(outfile,address,SEEK_SET);
      fwrite(&c,1,1,outfile);

      address=i+0x200;
      v=palette[i].b;
      c=(v>>4)|((v&0xf)<<4);
      fseek(outfile,address,SEEK_SET);
      fwrite(&c,1,1,outfile);
    }
    
    
    if (outfile != NULL) {
      fclose(outfile);
      outfile = NULL;
    }

  }


  /* ============================ */
  if (mode==1) {

    unsigned char first_half[1024];
    
    printf("mode=1 (charrom)\n");
    // charrom mode

    int vhdl_mode=1;
    if (!strstr(outputfilename,".vhdl")) vhdl_mode=0;
    
    int bytes=0;
    if (vhdl_mode) fprintf(outfile,"%s",vhdl_prefix);
    if (width!=8) {
      fprintf(stderr,"Fonts must be 8 pixels wide\n");
    }

    int spots[8][8];
    int charsets;


    // 4KB = 2x 256 char = 2KB charsets
    for(charsets = 0 ; charsets<2 ; charsets++) {
      if (bytes>=4096) break;
      fprintf(stdout,"yheight=%d\n",height);
      for (y=0; y<height; y++) {
	png_byte* row = row_pointers[y];
	int byte=0;
	int yy=y&7;
	
	for (x=0; x<width; x++) {
	  png_byte* ptr = &(row[x*multiplier]);
	  int r=ptr[0],g=ptr[1],b=ptr[2]; //, a=ptr[3];
	  
	  if (x<8) {
	    if (r>0x7f||g>0x7f||b>0x7f) {
	      byte|=(1<<(7-x));
	      spots[yy][x]=1;
	    } else spots[yy][x]=0;
	  }
	}
	fflush(stdout);
	char comma = ',';
	if (bytes<1024) {
	  first_half[bytes]=byte;
	}
	bytes++;
	if (bytes>=4096) {
	  comma=' ';
	}
	if (vhdl_mode) fprintf(outfile,"x\"%02x\"%c",byte,comma);
	else fputc(byte,outfile);
	if (vhdl_mode) {
	  if ((y&7)==7) {
	    fprintf(outfile,"\n");
	    int yy;
	    for(yy=0;yy<8;yy++) {
	      fprintf(outfile,"-- [");
	      for(x=0;x<8;x++) {
		if (spots[yy][x]) fprintf(outfile,"*"); else fprintf(outfile," ");
	      }
	      fprintf(outfile,"]\n");
	    }
	  }
	}
      }

      // Fill in any missing bytes
      if (bytes<2048) {

	printf("Padding output file to 2048 after first charset\n");

	if (vhdl_mode) {
	  fprintf(outfile,",\n");

	for(;bytes<2048;bytes+=8) {
	  int reverse=bytes&0x400;
	  if (reverse) reverse=0xff;
	  
	  fprintf(outfile,"x\"%02X\",x\"%02X\",x\"%02X\",x\"%02X\",x\"%02X\",x\"%02X\",x\"%02X\",x\"%02X\"%c -- 0x%03x (set %d, char 0x%02x)\n",
		  first_half[(bytes+0)&0x3ff]^reverse,
		  first_half[(bytes+1)&0x3ff]^reverse,
		  first_half[(bytes+2)&0x3ff]^reverse,
		  first_half[(bytes+3)&0x3ff]^reverse,
		  first_half[(bytes+4)&0x3ff]^reverse,
		  first_half[(bytes+5)&0x3ff]^reverse,
		  first_half[(bytes+6)&0x3ff]^reverse,
		  first_half[(bytes+7)&0x3ff]^reverse,		  
		  ',',
		  bytes,bytes/2048,(bytes/8)&0xff);

	  fprintf(outfile,"\n");
	  int yy;
	  for(yy=0;yy<8;yy++) {
	    fprintf(outfile,"-- [");
	    for(x=0;x<8;x++) {
	      if ((first_half[(bytes+yy)&0x3ff]^reverse)&(1<<(7-x))) fprintf(outfile,"*"); else fprintf(outfile," ");
	    }
	    fprintf(outfile,"]\n");
	  }
	}
	} else {
	  // In raw mode, don't pad, or write charset twice
	  break;
	}
	
      }
      
    }
    // Fill in any missing bytes
    if (bytes<4096) {

      printf("Padding output file to 4096\n");

      if (vhdl_mode) {
	fprintf(outfile,",\n");
	for(;bytes<4096;bytes+=8) {
	  int reverse=bytes&0x400;
	  if (reverse) reverse=0xff;
	  
	  fprintf(outfile,"x\"%02X\",x\"%02X\",x\"%02X\",x\"%02X\",x\"%02X\",x\"%02X\",x\"%02X\",x\"%02X\"%c -- 0x%03x (set %d, char %d)\n",
		  first_half[(bytes+0)&0x3ff]^reverse,
		  first_half[(bytes+1)&0x3ff]^reverse,
		  first_half[(bytes+2)&0x3ff]^reverse,
		  first_half[(bytes+3)&0x3ff]^reverse,
		  first_half[(bytes+4)&0x3ff]^reverse,
		  first_half[(bytes+5)&0x3ff]^reverse,
		  first_half[(bytes+6)&0x3ff]^reverse,
		  first_half[(bytes+7)&0x3ff]^reverse,		  
		  bytes<(4096-8)?',':' ',
		  bytes,bytes/2048,(bytes/8)&0xff);
	}
      } else {
	// In raw mode, don't pad
      }
    }
    if (vhdl_mode) fprintf(outfile,"%s",vhdl_suffix);

    if (outfile != NULL) {
      fclose(outfile);
      outfile = NULL;
    }
  }

  /* ============================ */
  if (mode==2) {
    printf("mode=2 (hi-res prep)\n");
    // hi-res image preparation mode

    // int bytes=0;
    if (width%8||height%8) {
      fprintf(stderr,"Image must be multiple of 8 pixels wide and high\n");
    }
    int problems=0;
    int total=0;
    int threes=0;
    int fours=0;
    int ones=0;

    int tiles[8000][8][8];
    int tile_count=0;

    int this_tile[8][8];

    for (y=0; y<height; y+=8) {
      for (x=0; x<width; x+=8) {
	int yy,xx;
	int i;
	int colour_count=0;
	int colours[64];

	printf("[%d,%d]\n",x,y);

	total++;

	for(yy=y;yy<y+8;yy++) {
	  png_byte* row = row_pointers[yy];
	  for(xx=x;xx<x+8;xx++) {
	    png_byte* ptr = &(row[xx*multiplier]);
	    int r,g,b;
	    if (ptr&&(yy<height)&&(xx<width)) {
	      r=ptr[0]; g=ptr[1]; b=ptr[2];
	    } else {
	      r=0; g=0; b=0;
	    }
	    int c=r+256*g+65536*b;
	    this_tile[yy-y][xx-x]=c;
	    for(i=0;i<colour_count;i++) if (c==colours[i]) break;
	    if (i==colour_count) {
	      colours[colour_count++]=c;
	    }
	  }
	}

	for(i=0;i<tile_count;i++) {
	  int dud=0;
	  int xx,yy;
	  for(xx=0;xx<8;xx++)
	    for(yy=0;yy<8;yy++) {
	      if (this_tile[yy][xx]!=tiles[i][yy][xx]) dud=1;
	    }
	  if (!dud) break;
	}
	if (i==tile_count) {
	  int xx,yy;
	  for(xx=0;xx<8;xx++)
	    for(yy=0;yy<8;yy++) {
	      tiles[tile_count][yy][xx]=this_tile[yy][xx];
	    }
	  printf(".[%d]",tile_count); fflush(stdout);
	  tile_count++;
	  if (tile_count>=8000) {
	    fprintf(stderr,"Too many tiles\n");
            if (outfile != NULL) {
              fclose(outfile);
              outfile = NULL;
            }
	    exit(-1);
	  }
	}

	if (colour_count==1) ones++;
	if (colour_count==3) threes++;
	if (colour_count==4) fours++;
	if (colour_count>2) {
	  printf("%d colours in card\n",colour_count);
	  problems++;
	}
      }
    }
    printf("%d problem tiles out of %d total tiles\n",problems,total);
    printf("%d with 3, %d with 4, %d with only one colour\n",threes,fours,ones);
    printf("%d unique tiles\n",tile_count);
  }

  if (mode==3) {
    // Output set of 16-colour sprites
    int colour_count=0;
    int colours[16];
    fprintf(stderr,"Scanning colour palette...\n");
    for (y=0; y<height; y++) {
      for (x=0; x<width; x++) {
	int i;

	png_byte* row = row_pointers[y];
	png_byte* ptr = &(row[x*multiplier]);
	int r=ptr[0], g=ptr[1],b=ptr[2]; // , a=ptr[3];
	int c=r+256*g+65536*b;
	for(i=0;i<colour_count;i++) if (c==colours[i]) break;
	if (i==colour_count) {
	  if (colour_count>=16) {
	    fprintf(stderr,"Too many colours. Image must be 16-colours or less.\n");
	    exit(-1);
	  }
	  colours[colour_count++]=c;
	}
      }
    }
    fprintf(stderr,"%d unique colours found.\n",colour_count);

    // Avoid colour 0 if we can, to keep it for transparency
    int colour_offset=0;
    //    if (colour_count<16) colour_offset=1;


    int bytes_per_sprite=8*(height>>1);
    while (bytes_per_sprite%64) bytes_per_sprite++;
    fprintf(stderr,"Sprites are %d ($%02X) pixels high, and will occupy %d bytes (%d VIC-II sprite slots) each.\n",
	    height/2,height/2,bytes_per_sprite,bytes_per_sprite/64);    

    
    // Output palette
    unsigned char red[16];
    unsigned char green[16];
    unsigned char blue[16];
    int i;
    
    for(i=0;i<16;i++)
      {
	int idx=i;
	if (colour_offset) idx+=colour_offset;
	if (idx>=16) idx-=16;

	red[idx]=nyblswap(colours[i]&0xff);
	green[idx]=nyblswap((colours[i]>>8)&0xff);
	blue[idx]=nyblswap((colours[i]>>16)&0xff);
      }
    // Write magic string, height of sprites in pixels,
    // and the number of sprites, and how many slots per
    // sprite
    fwrite("M65SPRITE16",12,1,outfile);
    fputc(height/2,outfile);
    fputc(width/32,outfile);
    fputc(bytes_per_sprite&0xff,outfile);
    fputc(bytes_per_sprite>>8,outfile);
    fwrite(red,16,1,outfile);
    fwrite(green,16,1,outfile);
    fwrite(blue,16,1,outfile);
    

    // Output pixels
    for (x=0; x<width; x+=32) {
      int bytes_written=0;
      int xx;
      fprintf(stderr,"Writing sprite %d\n",x/32);
      for (y=0; y<height; y+=2) {
	for(xx=x;xx<(x+32);xx+=4) {
	  unsigned char byte;
	  unsigned char p1,p2;
	  int i;

	  png_byte* row = row_pointers[y];
	  png_byte* ptr = &(row[(xx+0)*multiplier]);
	  int r=ptr[0], g=ptr[1],b=ptr[2]; // , a=ptr[3];
	  int c=r+256*g+65536*b;
	  for(i=0;i<colour_count;i++) if (c==colours[i]) break;
	  p1=i;
	  ptr = &(row[(xx+2)*multiplier]);	  
	  r=ptr[0]; g=ptr[1]; b=ptr[2]; // , a=ptr[3];
	  c=r+256*g+65536*b;
	  for(i=0;i<colour_count;i++) if (c==colours[i]) break;
	  p2=i;
	  byte=(p1<<4)+p2;
	  fputc(byte,outfile);
	  bytes_written++;
	}
      }
      while(bytes_written<bytes_per_sprite) {
	fputc(0x00,outfile); 
	bytes_written++;
      }
    }
    
  }
  
}

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

int main(int argc, char **argv)
{
  if (argc != 4) {
    fprintf(stderr,"Usage: program_name <logo|charrom|hires|sprite16> <file_in> <file_out>\n");
    exit(-1);
  }

  int mode=-1;

  if (!strcasecmp("logo",argv[1])) mode=0;
  if (!strcasecmp("charrom",argv[1])) mode=1;
  if (!strcasecmp("hires",argv[1])) mode=2;
  if (!strcasecmp("sprite16",argv[1])) mode=3;
  if (mode==-1) {
    fprintf(stderr,"Usage: program_name <logo|charrom|hires|sprite16> <file_in> <file_out>\n");
    exit(-1);
  }

  printf("argv[0]=%s\n", argv[0]);
  printf("argv[1]=%s\n", argv[1]);
  printf("argv[2]=%s\n", argv[2]);
  printf("argv[3]=%s\n", argv[3]);

  printf("Reading %s\n",argv[2]);
  read_png_file(argv[2]);

  printf("Processing with mode=%d and output=%s\n", mode, argv[3]);
  process_file(mode,argv[3]);

  printf("done\n");

  if (infile != NULL) {
    fclose(infile);
    infile = NULL;
  }

  if (outfile != NULL) {
    fclose(outfile);
    outfile = NULL;
  }

  return 0;
}

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