/*****************************************************************
 * fbham.c: FBM Release 1.0 25-Feb-90 Michael Mauldin
 *
 * Copyright (C) 1989,1990 by C. Harald Koch & Michael Mauldin.
 * Permission is granted to use this file in whole or in part for
 * any purpose, educational, recreational or commercial, provided
 * that this copyright notice is retained unchanged.  This software
 * is available to all free of charge by anonymous FTP and in the
 * UUNET archives.
 *
 * fbham.c: Write a 24bit RGB file as an Amiga HAM IFF file
 *
 * USAGE
 *     % fbham < image1 > image2
 *
 * EDITLOG
 *	LastEditDate = Mon Jun 25 00:56:54 1990 - Michael Mauldin
 *	LastFileName = /usr2/mlm/src/misc/fbm/fbham.c
 *
 * HISTORY
 * 13-Jul-89  Michael Mauldin (mlm) at Carnegie Mellon University
 *	Beta release (version 0.95) mlm@cs.cmu.edu
 *
 * 20-Apr-89  C. Harald Koch (chk) at DCIEM Toronto.
 *     Created.  chk@ben.dciem.dnd.ca
 *
 *================================================================
 * based on ray2.c from DBW_Render, Copyright 1987 David B. Wecker
 *
 * From: chk@dretor.dciem.dnd.ca (C. Harald Koch)
 * Subject: fbham.c - convert a 24bit FBM file to an IFF file using HAM mode
 * To: Michael.Mauldin@nl.cs.cmu.edu (Michael Maudlin)
 * Date: Mon, 1 May 89 17:21:16 EDT
 * X-Mailer: ELM [version 2.2 PL0]
 * 
 * This is the source to my program to convert from a 24bit FBM file to Amiga
 * HAM mode. It is based on Dave Wecker's RAY2 program, which converts the
 * output of his raytracer to HAM mode. His code uses the Amiga graphics
 * library to plot the pixels in a framebuffer for encoding; this version will
 * run standalone.
 * 
 * There may be bugs, although it works well for me here. It is probably not in
 * the format you want for FBM source; Please go ahead and modify it. There is
 * probably room for some command line options for various things (such as the
 * verbose output, run length encoding, etc) which I haven't needed and so
 * haven't added.
 * 
 * I will send you the IRIS programs when I get them. Enjoy!
 *	-chk
 *****************************************************************/

#include <stdio.h>
#include <math.h>
#include "fbm.h"

# define USAGE "Usage: fbham < image > image"

#ifndef lint
static char    *fbmid = 
"$FBM fbham.c <1.0> 25-Jun-90  (C) 1989 by C. Harald Koch, source \
code available free from MLM@CS.CMU.EDU and from UUNET archives$";

#endif

#define DEPTH	    6		/* max depth of Amiga HAM image */

#define BMHDsize    20L		/* chunk sizes */
#define CMAPsize    96L
#define CAMGsize    4L
#define BODYsize    ((long)(16000L))
#define FORMsize    (BODYsize+CAMGsize+CMAPsize+BMHDsize+36L)

unsigned char *planes[DEPTH];	/* bitplane pointers */

static int      comp = 1;	/* compress image? */
static int      depth = DEPTH;	/* depth of image being created */
static int      colors = 16;	/* number of colors in color lookup table */
static int      colorstats[4096][2];	/* color usage statistics */
static int	row_size;	/* number of bytes in IFF row of data */

/* temp variables for writing IFF file */
char            str[40];
long            lng, pos1, pos2;
short           wrd;
unsigned char   byt;
unsigned char  *dest, destbuf[BUFSIZ];


main(argc, argv)
char           *argv[];
{
    FBM             input;

    /* Clear the memory pointers so alloc_fbm won't be confused */
    input.cm = input.bm = (unsigned char *) NULL;

    /* Read the image and rotate it */
    if (!read_bitmap(&input, argc > 0 ? argv[1] : (char *) NULL))
    {
	exit(1);
    }

    /* slight sanity checks. could be better. */
    if (input.hdr.physbits != 8 || input.hdr.planes != 3) {
	fprintf(stderr, "input file must be 24 bit RGB data\n");
	exit(1);
    }

    /* convert to HAM */
    if (!fbm2ham(&input, stdout)) {
	exit(1);
    }

    exit(0);
}


/************************ run length encoding from Amiga RKM *****************/
#define DUMP		0   /* list of different bytes */
#define RUN		1   /* single run of bytes */
#define MinRun		3   /* shortest allowed run */
#define MaxRun		128 /* longest run (length is signed char) */
#define	MaxDat		128 /* longest block of unencoded data */
#define GetByte()	(*source++)
#define PutByte(c)	{ *dest++ = (c); ++PutSize; }
#define OutDump(nn)	dest = PutDump(dest,nn);
#define OutRun(nn,cc)	dest = PutRun(dest,nn,cc);

int             PutSize;
char            buf[256];

unsigned char  *
PutDump(dest, nn)
unsigned char  *dest;
int             nn;
{
    int             i;

    PutByte(nn - 1);
    for (i = 0; i < nn; i++)
	PutByte(buf[i]);
    return (dest);
}

unsigned char  *
PutRun(dest, nn, cc)
unsigned char  *dest;
int             nn, cc;
{
    PutByte(-(nn - 1));
    PutByte(cc);
    return (dest);
}

/* PackRow - pack a row of data using Amiga IFF RLE */
int 
PackRow(pSource, pDest, RowSize)
unsigned char **pSource, **pDest;
int             RowSize;
{
    unsigned char  *source, *dest;
    char            c, lastc = '\000';
    int             mode = DUMP, nbuf = 0,	/* number of chars in buf */
                    rstart = 0;	/* buf index current run starts */

    source = *pSource;
    dest = *pDest;

    PutSize = 0;
    buf[0] = lastc = c = GetByte();
    nbuf = 1;
    RowSize--;

    for (; RowSize; --RowSize) {
	buf[nbuf++] = c = GetByte();

	switch (mode) {
	case DUMP:
	    if (nbuf > MaxDat) {
		OutDump(nbuf - 1);
		buf[0] = c;
		nbuf = 1;
		rstart = 0;
		break;
	    }
	    if (c == lastc) {
		if (nbuf - rstart >= MinRun) {
		    if (rstart > 0)
			OutDump(rstart);
		    mode = RUN;
		}
		else if (rstart == 0)
		    mode = RUN;
	    }
	    else
		rstart = nbuf - 1;
	    break;

	case RUN:
	    if ((c != lastc) || (nbuf - rstart > MaxRun)) {
		OutRun((nbuf - 1) - rstart, lastc);
		buf[0] = c;
		nbuf = 1;
		rstart = 0;
		mode = DUMP;
	    }
	    break;
	}
	lastc = c;
    }
    switch (mode) {
    case DUMP:
	OutDump(nbuf);
	break;
    case RUN:
	OutRun(nbuf - rstart, lastc);
	break;
    }

    *pSource = source;
    *pDest = dest;

    return (PutSize);
}
/******************* end of RKM RL encoding routines **********************/


/* build_histogram - count frequency of 12bit colors in an FBM image */
build_histogram(image)
FBM *image;
{
    int i, j, t0, t1, gap, val, used;
    unsigned char *rp, *gp, *bp;
    int diff;

    /* initialize color statistics. */
    for (i = 0; i < 4096; i++) {
	colorstats[i][0] = i;
	colorstats[i][1] = 0;
    }

    /* obtain pointers to the beginning of each plane (Red, Green, Blue) */
    rp = image->bm;
    gp = rp + image->hdr.plnlen;
    bp = gp + image->hdr.plnlen;

    /* count the number of occurences of each color in the image */
    for (i = 0 ; i < image->hdr.plnlen ; i++) {
	val = ((*rp++ & 0xf0) << 4) + (*gp++ & 0xf0) + ((*bp++ & 0xf0) >> 4);
	val %= 4096;
	if (colorstats[val][1] < 32767) {
	    colorstats[val][1]++;
	}
    }

    /* sort the color stats in order of decreasing usage */
    for (gap = 2048; gap > 0; gap /= 2) {
	for (i = gap; i < 4096; i++) {
	    for (j = i - gap; j >= 0 &&
		    colorstats[j][1] < colorstats[j + gap][1]; j -= gap) {
		t0 = colorstats[j][0];
		t1 = colorstats[j][1];
		colorstats[j][0] = colorstats[j + gap][0];
		colorstats[j][1] = colorstats[j + gap][1];
		colorstats[j + gap][0] = t0;
		colorstats[j + gap][1] = t1;
	    }
	}
    }

    /* count the number of colors actually used in the image */
    for (used = 0; used < 4096 && colorstats[used][1] > 0; used++);
    fprintf(stderr, "Used %d colors out of a possible 4096\n", used);
}

/*-
 * plot_pixel - plot a color in an Amiga style bitmap (one plane per bit)
 *
 * Description:
 *	A somewhat optimized routine to set/reset one bit in each plane
 *	at the specified (x,y) coordinates. plane i gets the bit in
 *	position i of color. a replacement for the Amiga WritePixel call.
-*/
plot_pixel(planes, x, y, color)
unsigned char *planes[];
int x, y;
register int color;
{
    register int bit;
    register unsigned char shifted_bit = 1 << (7-(x%8));
    register int array_offset = y * row_size + x/8;
    register int i;

    for (i = 0; color && i < depth; i++) {
	bit = color & 1;
	color >>= 1;

	if (bit)    *(planes[i] + array_offset) |= shifted_bit;
    }
}


/*-
 * fbm2ham - write an FBM image in HAM IFF format on the given file pointer.
 *
-*/
fbm2ham(image, ofil)
FBM *image;
FILE *ofil;
{
    int             i, j, k, gap, t0, t1, prgb, crgb, cpix, ppix, maxdis, used;
    int		    c1, c2, diff;
    unsigned char *rp, *gp, *bp;

    fprintf(stderr, "Building a color histogram:\n");
    build_histogram(image);

    /* convert the image to an Amiga HAM bitplane based image */
    cpix = 0;
    crgb = colorstats[0][1];

    rp = image->bm;
    gp = rp + image->hdr.plnlen;
    bp = gp + image->hdr.plnlen;

    row_size = ((image->hdr.cols + 15) / 16) * 2;
    diff = image->hdr.rowlen - image->hdr.cols;

    for (i = 0; i < depth ; i++) {
	planes[i] = (unsigned char *)malloc(row_size * image->hdr.rows);
    }

    for (i = 0; i < image->hdr.rows; i ++) {
	/* verbose output because this program is slow even on a Sun */
	if (i%50 == 0) { fprintf(stderr, "...%d", i); fflush(stderr); }

	/* step through current row, converting FBM pixel to nearest equivalent
	 * HAM pixel */
	for (j = 0; j < image->hdr.cols; j++) {
	    prgb = crgb;
	    crgb = ((*rp++ & 0xf0) << 4) + (*gp++ & 0xf0) + ((*bp++ & 0xf0) >> 4);
	    crgb %= 4096;
	    ppix = cpix;

	    /* start of scan line is ALWAYS an absolute color */
	    if (j == 0)
		cpix = getcolor(ppix, &crgb, -1);
	    else
		cpix = getcolor(ppix, &crgb, prgb);

	    /* plot the computed pixel */
	    plot_pixel(planes, j, i, cpix);
	}
	rp += diff;
	gp += diff;
	bp += diff;
    }
    fprintf(stderr, "\n");

    /* Now we write the planes[] array we just created in ILBM format */
    sprintf(str, "FORM");
    fwrite(str, 1, 4, ofil);
    pos1 = ftell(ofil);
    lng = FORMsize;
    fwrite(&lng, 4, 1, ofil);
    sprintf(str, "ILBM");
    fwrite(str, 1, 4, ofil);

    sprintf(str, "BMHD");
    fwrite(str, 1, 4, ofil);
    lng = BMHDsize;
    fwrite(&lng, 4, 1, ofil);
    wrd = image->hdr.cols;
    fwrite(&wrd, 2, 1, ofil);	/* width */
    wrd = image->hdr.rows;
    fwrite(&wrd, 2, 1, ofil);	/* height */
    wrd = 0;
    fwrite(&wrd, 2, 1, ofil);	/* top */
    wrd = 0;
    fwrite(&wrd, 2, 1, ofil);	/* left */
    byt = depth;
    fwrite(&byt, 1, 1, ofil);	/* Depth */
    byt = 0;
    fwrite(&byt, 1, 1, ofil);	/* mask */
    byt = comp;
    fwrite(&byt, 1, 1, ofil);	/* compress */
    byt = 0;
    fwrite(&byt, 1, 1, ofil);	/* pad */
    wrd = 0;
    fwrite(&wrd, 2, 1, ofil);	/* transparency */
    byt = 10;
    fwrite(&byt, 1, 1, ofil);	/* aspect x */
    byt = 11;
    fwrite(&byt, 1, 1, ofil);	/* aspect y */
    wrd = image->hdr.cols;
    fwrite(&wrd, 2, 1, ofil);	/* page width */
    wrd = image->hdr.rows;
    fwrite(&wrd, 2, 1, ofil);	/* page height */

    /* CAMG chunk for displaying files on the Amiga. should include at least
     * the HAM flag; I also add lace if the picture is large enough. Perhaps
     * this should be an option. -CHK */
    sprintf(str, "CAMG");
    fwrite(str, 1, 4, ofil);
    lng = CAMGsize;
    fwrite(&lng, 4, 1, ofil);
    if (image->hdr.rows > 200)
	lng = 0x804L;		/* HAM | LACE */
    else
	lng = 0x800L;		/* HAM */
    fwrite(&lng, 4, 1, ofil);

    /* write out the color lookup table */
    sprintf(str, "CMAP");
    fwrite(str, 1, 4, ofil);
    lng = CMAPsize;
    fwrite(&lng, 4, 1, ofil);
    for (i = 0; i < 32; i++) {
	str[0] = (colorstats[i][0] >> 4) & 0xF0;
	str[1] = (colorstats[i][0]) & 0xF0;
	str[2] = (colorstats[i][0] << 4) & 0xF0;
	fwrite(str, 1, 3, ofil);
    }

    sprintf(str, "BODY");
    fwrite(str, 1, 4, ofil);
    pos2 = ftell(ofil);
    lng = BODYsize;
    fwrite(&lng, 4, 1, ofil);

    lng = 0L;

    for (i = 0; i < image->hdr.rows; i ++) {
	for (j = 0; j < depth; j++) {
	    if (comp) {
		dest = destbuf;
		wrd = PackRow(&planes[j], &dest, row_size);
		lng += (long) wrd;
		fwrite(destbuf, 1, wrd, ofil);
	    }
	    else {
		fwrite(planes[j], 1, row_size, ofil);
		planes[j] += row_size;
	    }
	}
    }

    /* make BODY a multiple of 4 bytes in length */
    byt = 0;
    if (comp)
	while (lng % 4) {
		fwrite(&byt, 1, 1, ofil);	/* pad */
		lng++;
	}

    if (comp) {
	fseek(ofil, (long) pos2, 0);
	fwrite(&lng, 4, 1, ofil);
	lng -= BODYsize;
	lng += FORMsize;
	fseek(ofil, (long) pos1, 0);
	fwrite(&lng, 4, 1, ofil);
    }

    return 1;
}

/* get the next encoding for a pixel, based on the previous pixel and the
 * current 12 bit RGB value */

#define BPP	    4		/* Bits per pixel */
#define MAXGRAY	    (1 << BPP)
#define	ABS(x)	    ((x) < 0 ? -(x) : (x))

/*-
 *	first, check to see if pixel is the same color. if so, return
 *	check if only one of R, G, B have changed. if so, return new pixel.
 *	find closest entry in colormap. if exact match, return it.
 *	modify one of R, G, B (whichever difference is largest)
 *	compare previous calculation to best match in color table. return
 *	    whichever is a better match.
-*/
getcolor(ppix, crgb, prgb)
int             ppix, *crgb, prgb;
{
    int             i, j, val, cr, cg, cb, pr, pg, pb, nr, ng, nb, best, dist, nrgb;

    /* if same color, then do a NOOP (same as previous pixel) */
    if (*crgb == prgb)
	return (ppix);

    /* set up for comparisons */
    cb = *crgb & (MAXGRAY - 1);
    cg = (*crgb >> BPP) & (MAXGRAY - 1);
    cr = (*crgb >> (BPP * 2)) & (MAXGRAY - 1);

    pb = prgb & (MAXGRAY - 1);
    pg = (prgb >> BPP) & (MAXGRAY - 1);
    pr = (prgb >> (BPP * 2)) & (MAXGRAY - 1);

    /* see if only one plane changed, if so, use HAM encoding */
    if (prgb != -1) {
	if (pr == cr && pg == cg)
	    return (cb + 0x10);
	if (pr == cr && pb == cb)
	    return (cg + 0x30);
	if (pg == cg && pb == cb)
	    return (cr + 0x20);
    }

    /* else look for an exact match in the color table (or minimal distance) */
    for (i = 0; i < colors; i++) {
	if (colorstats[i][0] == *crgb)
	    return (i);
	if (i == 0) {
	    best = 0;
	    dist = distance(colorstats[i][0], *crgb);
	}
	else if ((j = distance(colorstats[i][0], *crgb)) < dist) {
	    best = i;
	    dist = j;
	}
    }

    /* do a forced absolute */
    if (prgb == -1) {
	*crgb = colorstats[best][0];
	return (best);
    }

    /* find which color is off the most from previous */
    i = 0;
    val = ABS(cr - pr);
    if (ABS(cg - pg) > val) {
	i = 1;
	val = ABS(cg - pg);
    }
    if (ABS(cb - pb) > val) {
	i = 2;
	val = ABS(cb - pb);
    }

    nr = pr;
    ng = pg;
    nb = pb;
    switch (i) {
    case 0:
	nr = cr;
	val = nr + 0x20;
	break;
    case 1:
	ng = cg;
	val = ng + 0x30;
	break;
    case 2:
	nb = cb;
	val = nb + 0x10;
	break;
    }
    nrgb = (nr << (2 * BPP)) + (ng << BPP) + nb;

    /* now pick the best */
    if (distance(nrgb, *crgb) < dist) {

	/* do a best relative */
	*crgb = nrgb;
	return (val);
    }

    /* do a best absolute */
    *crgb = colorstats[best][0];
    return (best);
}

/* calculate distance between two colors in 3D space */
distance(argb, brgb)
{
    int             b, g, r;

    /* set up for comparisons */
    b = argb & (MAXGRAY - 1);
    g = (argb >> BPP) & (MAXGRAY - 1);
    r = (argb >> (BPP * 2)) & (MAXGRAY - 1);

    b -= brgb & (MAXGRAY - 1);
    g -= (brgb >> BPP) & (MAXGRAY - 1);
    r -= (brgb >> (BPP * 2)) & (MAXGRAY - 1);

    return (r * r + g * g + b * b);
}
