/* Compile: cc -O2 -o mpegtty mpegtty.c -lcl */

#include <stdio.h>
#include <stdlib.h>
#include <dmedia/cl.h>
#include <fcntl.h>
#include <unistd.h>

#ifdef DUMP_CHAR
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/cursorfont.h>
#endif /* DUMP_CHAR */

char *mpegtty_version = "1.1";

#define DITHER_SIZE      5

#if defined(sgi)
#define INLINE __inline
#elif defined(__GNUC__)
#define INLINE inline
#endif

typedef struct _AsciiInfo
{
    int ch;
    int n;
} AsciiInfo;

typedef struct _Asciis
{
    int n;
    int cur;
    char *s;
    int conc;
} Asciis;

struct _dither {
    double **data;
    int n;
};

typedef struct _dither* dither;

#ifndef Bool
#define Bool int
#endif

#ifdef DUMP_CHAR
Display *disp;
char *hostname = NULL;
char *font_name = "a14";
#endif /* DUMP_CHAR */

char *image_chars = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";

int group = 10;
dither dith;
int reverse_mode = 0;
int imageWidth, imageHeight;
int frame_no;
int color_mode = 0;

dither create_dither(int dn)
{
    int n, i, j, k;
    dither d;
    double **data, base;

    d = (struct _dither*)malloc(sizeof(struct _dither));
    n = 2 << dn;
    d->n = n;
    data = d->data = (double **)malloc(sizeof(double *) * n);
    for (i = 0; i < n; i++) {
	data[i] = d->data[i] = (double *)malloc(sizeof(double) * n);
	for (j = 0; j < n; data[i][j++] = 0)
	    ;
    }

    data[0][0] = 0;
    data[1][1] = 1;
    data[0][1] = 2;
    data[1][0] = 3;

    for (k = 2; k < n; k <<= 1) {
	for (i = 0; i < k; i++) {
	    for (j = 0; j < k; j++) {
		base = (data[i][j] *= 4);
		data[i + k][j + k] = base + 1;
		data[i][j + k] = base + 2;
		data[i + k][j] = base + 3;
	    }
	}
    }

    for (i = 0; i < n; i++)
	for (j = 0; j < n; j++)
	    data[i][j] /= (double)(n * n);

    return d;
}

void free_dither(dither d)
{
    int i;

    for (i = 0; i < d->n; i++)
	free(d->data[i]);
    free(d);
}

static INLINE Bool is_hit_dither(dither d, double data, int x, int y)
{
    register int n = d->n;

    return d->data[x % n][y % n] < data;
}

void set_char_concentration(AsciiInfo *ascii, char *s, int n)
{
#ifdef DUMP_CHAR
    int i;
    int char_w, char_h;
    int x_off, y_off;
    XFontStruct *fs;
    GC gc;
    Pixmap pm;
    XImage *im;
    int x, y;

    if((fs = XLoadQueryFont(disp, font_name)) == NULL)
    {
	fprintf(stderr, "Can't load font `%s'\n", font_name);
	exit(1);
    }

    char_w = fs->max_bounds.rbearing - fs->min_bounds.lbearing;
    char_h = fs->max_bounds.ascent + fs->max_bounds.descent;
    x_off  = -fs->min_bounds.lbearing;
    y_off  = fs->max_bounds.ascent;

    /* initialize pixmap for loading font image */
    pm = XCreatePixmap(disp, RootWindow(disp, 0), char_w * n, char_h,
		       DefaultDepth(disp, 0));
    gc = XCreateGC(disp, pm, 0, NULL);
    XSetForeground(disp, gc, 1);
    XSetBackground(disp, gc, 0);
    XSetFont(disp, gc, fs->fid);
    XDrawImageString(disp, pm, gc, x_off, y_off, s, n);
    im = XGetImage(disp, pm, 0, 0, char_w * n, char_h, AllPlanes, ZPixmap);

    for(i = 0; i < n; i++)
    {
	ascii[i].ch = s[i];
	if(s[i] == ' ')
	{
	    ascii[i].n = 0;
	    continue;
	}

	for(y = 0; y < char_h; y++)
	{
	    for(x = char_w * i; x < char_w * (i + 1); x++)
	    {
		ascii[i].n += XGetPixel(im, x, y);
	    }
	}
    }

    XDestroyImage(im);
    XFreePixmap(disp, pm);
    XFreeFont(disp, fs);
    XFreeGC(disp, gc);
#else
    static int char_conc[128] =
    {
	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 10, 28, 29, 26, 24, 5, 13, 13, 15,
	11, 5, 6, 5, 13, 20, 16, 20, 22, 21, 25, 25, 17, 24, 25, 10, 9, 9, 12,
	9, 14, 30, 24, 28, 20, 26, 23, 18, 25, 24, 18, 16, 20, 15, 28, 26, 24,
	22, 30, 26, 22, 16, 22, 20, 28, 20, 15, 20, 19, 13, 19, 6, 6, 5, 21,
	23, 15, 23, 21, 16, 24, 20, 14, 16, 18, 15, 20, 17, 18, 22, 22, 13,
	16, 16, 17, 12, 18, 14, 22, 17, 15, 13, 15, 8, 0
    };
    int i;

    for(i = 0; i < n; i++)
    {
	ascii[i].ch = s[i];
	ascii[i].n  = char_conc[s[i]];
    }
#endif /* DUMP_CHAR */
}

int calc_concentration_rank(AsciiInfo *ascii, int n)
{
    int i, rank, conc;

    i = 0;
    rank = 0;

    while(i < n)
    {
	conc = ascii[i].n;
	while(i < n && ascii[i].n == conc)
	    i++;
	rank++;
    }

    return rank;
}

#ifdef DUMP_CHAR
void open_display(void)
{
    if((disp = XOpenDisplay(hostname)) == NULL)
    {
	fprintf(stderr, "Can't open display `%s'\n", XDisplayName(hostname));
	exit(1);
    }
}

void close_display(void)
{
    XCloseDisplay(disp);
}
#endif /* DUMP_CHAR */

int cmp_ascii(const AsciiInfo *a1, const AsciiInfo *a2)
{
    return a1->n - a2->n;
}

void sort_ascii(AsciiInfo *ascii, int n)
{
    qsort(ascii, n, sizeof(AsciiInfo), (int (*)(const void*,const void*))cmp_ascii);
}

Asciis *make_asciis(char *s, int *rank_return)
{
    AsciiInfo *ascii;
    Asciis *as;
    int n;
    int rank;
    int i, j, k, x, conc, r;

    int cval[128];

    n = strlen(s);
    ascii = (AsciiInfo *)malloc(sizeof(AsciiInfo) * n);
    set_char_concentration(ascii, image_chars, n);

#ifdef DUMP_CHAR
    memset(cval, 0, sizeof(cval));
    for(i = 0; i < n; i++)
	cval[ascii[i].ch] = ascii[i].n;
    for(i = 0; i < 128; i++)
	printf("%d, ", cval[i]);
    printf("\n");
    exit(0);
#endif

    sort_ascii(ascii, n);
    rank = calc_concentration_rank(ascii, n);
    as = (Asciis *)malloc(sizeof(Asciis) * rank);

    j = 0;
    for(i = 0; i < rank; i++)
    {
	conc = ascii[j].n;
	k = 0;
	while(j < n && ascii[j].n == conc)
	{
	    j++;
	    k++;
	}
	as[i].n = k;
	as[i].cur = 0;
	as[i].s = (char *)malloc(k + 1);
	as[i].conc = conc;
	for(x = 0; x < k; x++)
	    as[i].s[x] = ascii[j - k + x].ch;
    }

    free(ascii);

    *rank_return = rank;
    return as;
}

static INLINE int select_ascii(Asciis *as)
{
    int ch;

    ch = as->s[as->cur];
    as->cur = (as->cur + 1) % as->n;

    return ch;
}

/* 0.0 <= conc <= 1.0 */
int get_ascii(Asciis *as, int n, double conc, int x, int y)
{
    int left, right;
    double t;
    int ndiv;
    double a;

    ndiv = n - 1;
    a = 1.0 / ndiv;
    t = conc * 0.9999 / a;
    
    left = (int)t;
    right = (int)(t + 1);

    if(left == right)
	return select_ascii(as + left);
    else
    {
	t = (conc - left * a) / a;
	if(is_hit_dither(dith, t, x, y))
	    return select_ascii(as + right);
    }
    return select_ascii(as + left);
}

static INLINE double calc_color_value(int frame_picel)
{
    double r, g, b;

    r = (((frame_picel & 0x00ff0000) >> 16)) / 256.0;
    g = (((frame_picel & 0x0000ff00) >>  8)) / 256.0;
    b = (((frame_picel & 0x000000ff) >>  0)) / 256.0;

    if(reverse_mode)
	return 1 - (0.299 * r + 0.587 * g + 0.114 * b);	/* 0.0 - 1.0 */
    return 0.299 * r + 0.587 * g + 0.114 * b;		/* 0.0 - 1.0 */
}

static int last_rgb = -1;
static INLINE void set_color(unsigned *frame,
			     int w, int h, int x, int y, int scale)
{
    unsigned pic;
    int rgb;
    char buff[6];
    double d, r, g, b;

    pic = frame[(y * w + x) * scale];

    if((pic & 0x00ffffff) == 0)
	rgb = 0;
    else
    {
	r = ((pic>> 0) & 0xff);
	g = ((pic>> 8) & 0xff);
	b = ((pic>>16) & 0xff);

	if(r < g)
	    d = g;
	else
	    d = r;
	if(d < b)
	    d = b;
	d = (1.0 / d);

	r *= d;
	g *= d;
	b *= d;

	if(r < 0.90 || g < 0.90 || b < 0.90)
	{
	    if(r < g && r < b)
		r = 0;
	    else if(g < r && g < b)
		g = 0;
	    else
		b = 0;
	}
	rgb = 0;
	if(r > 0.5) rgb |= 1;
	if(g > 0.5) rgb |= 2;
	if(b > 0.5) rgb |= 4;
    }

    if(last_rgb == rgb)
	return;

    buff[0] = '\033';
    buff[1] = '[';
    buff[2] = '3';
    buff[3] = '0' + rgb;
    buff[4] = 'm';
    buff[5] = '\0';
    fputs(buff, stdout);
    last_rgb = rgb;
}

void reset_color(void)
{
    fputs("\033[0m", stdout);
    last_rgb = -1;
}

double **screen_concentrations(int* frame, int iw, int ih,
			       int *ww, int *hh, int g)
{
    static double **data = NULL;
    static int w, h;
    int i, xx, yy, x, y;
    double d, gg;

    w = *ww = iw / g - 1;
    h = *hh = ih / g - 1;
    if(data == NULL)
    {
	/* allocate data */
	data = (double **)malloc(sizeof(double *) * h);
	for(i = 0; i < h; i++)
	    data[i] = (double *)malloc(sizeof(double) * w);
    }

    gg = 1.0 / (g * g);
    for(y = 0; y < h; y++)
    {
	for(x = 0; x < w; x++)
	{
	    d = 0.0;
	    for(yy = 0; yy < g; yy++)
	    {
		for(xx = 0; xx < g; xx++)
		{
		    d += calc_color_value(frame[(y * g + yy) * iw +
						x * g + xx]);
		}
	    }
	    data[y][x] = d * gg;
	}
    }

    return data;
}

void dump_image(Asciis* as, int nascii, void* frame)
{
    int* p = (int *)frame;
    int x, y, w, h, n;
    double** data;
    double cmax, cmin, cb;

    data = screen_concentrations((int *)frame,
				 imageWidth, imageHeight,
				 &w, &h, group);

    cmax = cmin = data[0][0];
    for(y = 0; y < h; y++)
    {
	for(x = 0; x < w; x++)
	{
	    if(data[y][x] > cmax)
		cmax = data[y][x];
	    else if(data[y][x] < cmin)
		cmin = data[y][x];
	}
    }
    cb = cmax - cmin;

    putchar('\033');
    putchar('[');
    putchar('H');
    printf("%d\n", frame_no);

    for(y = h - 1; y >= 0; y--)
    {
	for(x = 0; x < w; x++)
	{
	    int c1, c2;

	    if(cb < 0.0001)
	    {
		c1 = get_ascii(as, nascii, cmin, x, y);
		c2 = get_ascii(as, nascii, cmin, x, y);
	    }
	    else
	    {
		double conc;

		conc = (data[y][x] - cmin) / cb;
		c1 = get_ascii(as, nascii, conc, x, y);
		c2 = get_ascii(as, nascii, conc, x, y);
	    }
	    if(color_mode)
		set_color((unsigned *)frame,
			  imageWidth, imageHeight,
			  x, y, group);
	    putchar(c1);
	    putchar(c2);

	}
	putchar('\n');
    }
    if(color_mode)
	reset_color();
    fflush(stdout);
}

void print_asciis(Asciis *as, int n)
{
    int i, j;

    for(i = 0; i < n; i++)
    {
	printf("%d:%d<%d>:", i, as[i].conc, as[i].n);
	for(j = 0; j < as[i].n; j++)
	    printf(" `%c'", as[i].s[j]);
	printf("\n");
    }
}

void usage(void)
{
#ifdef DUMP_CHAR
    puts(
"mpegtty [-display <display>] [-pixel <size>] "
"[-c <image-characters>] [-r] [-h] MPEG-file"
);
#else
    puts(
"mpegtty [-pixel <size>] [-color]"
"[-c <image-characters>] [-r] [-h] MPEG-file"
);
#endif /* DUMP_CHAR */
}


void main(int argc, char** argv)
{
    char* fname;
    CLhandle decompressorHdl;
    int decompressionScheme;
    int inFile;
    char* header;
    int headerSize;
    int compressedBufferSize;
    CLbufferHdl compressedBufferHdl;
    CLbufferHdl frameBufferHdl;
    void* frameBuffer;
    Asciis *as;
    int nascii;

    argc--;
    argv++;
    for(;; argv++, argc--)
    {
	if(!strcmp(*argv, "-pixel"))
	{
	    argv++, argc--;
	    group = atoi(*argv);
	}
#ifdef DUMP_CHAR
	else if(!strcmp(*argv, "-display"))
	{
	    argv++, argc--;
	    hostname = *argv;
	}
#endif /* DUMP_CHAR */
	else if(!strcmp(*argv, "-c"))
	{
	    argv++, argc--;
	    image_chars = *argv;
	}
	else if(!strcmp(*argv, "-v"))
	{
	    printf("mpegtty version %s\n", mpegtty_version);
	    exit(0);
	}

	else if(!strcmp(*argv, "-r"))
	{
	    reverse_mode = 1;
	}
	else if(!strcmp(*argv, "-color"))
	{
	    color_mode = 1;
	}
	else if(!strcmp(*argv, "-h"))
	{
	    usage();
	    exit(0);
	}
	else
	    break;
    }

    if(argc == 0)
    {
	usage();
	exit(1);
    }

#ifdef DUMP_CHAR
    open_display();
#endif /* DUMP_CHAR */

    dith = create_dither(DITHER_SIZE);
    as = make_asciis(image_chars, &nascii);

/*    print_asciis(as, nascii); exit(0); */

    fname = *argv;
    inFile = open(fname, O_RDONLY);
    if(inFile < 0)
    {
	perror(fname);
	exit(1);
    }

    /* Determine the scheme from the first 16 bytes of the header */
    header = (char *)malloc(16);
    read(inFile, header, 16);
    decompressionScheme = clQueryScheme(header);
    if(decompressionScheme < 0)
    {
	fprintf(stderr, "Unknown scheme in stream header.\n");
	exit(1);
    }
    free(header);

    /* Open the appropriate decompressor */
    clOpenDecompressor(decompressionScheme, &decompressorHdl);

    /* Find out how much header information to provide */
    headerSize = clQueryMaxHeaderSize(decompressionScheme);

    if(headerSize > 0)
    {
	/* Get the header data */
	header = (char *)malloc(headerSize);
	lseek(inFile, 0, SEEK_SET);
	read(inFile, header, headerSize);

	/* Read the header */
	clReadHeader(decompressorHdl, headerSize, header);
	free(header);

	/* Reset the stream */
	lseek(inFile, 0, SEEK_SET);
    }

    imageWidth  = clGetParam(decompressorHdl, CL_IMAGE_WIDTH);
    imageHeight = clGetParam(decompressorHdl, CL_IMAGE_HEIGHT);
/*    printf("## W/H = %d/%d\n", imageWidth, imageHeight); */

    /* Allocate space for compressed buffer */
    compressedBufferSize = clGetParam(decompressorHdl,
				      CL_COMPRESSED_BUFFER_SIZE);
/*    compressedBuffer = malloc(compressedBufferSize); */

    frameBufferHdl = clCreateBuf(decompressorHdl, CL_BUF_FRAME,
				 12, 4 * imageWidth * imageHeight, NULL);
    compressedBufferHdl = clCreateBuf(decompressorHdl, CL_BUF_COMPRESSED,
				      compressedBufferSize, 1, NULL);


    frame_no = 0;
    fputs("\033[H\033[J", stdout);
    fflush(stdout);
    while(1)
    {
	int size, wrap;
	void* buf;

/*	printf("## frame=%d\n", frame_no); */
	while((size = clQueryFree(compressedBufferHdl, 0, &buf, &wrap)) > 0)
	{
	    if(read(inFile, buf, size) <= 0)
		goto done;
	    clUpdateHead(compressedBufferHdl, size);
	}
	/* Decompress a frame */
	clDecompress(decompressorHdl, 1, 0, NULL, NULL);
	/* Read the frame */
	size = clQueryValid(frameBufferHdl, 1, &frameBuffer, &wrap);
	frame_no++;
/*	printf("## size=%d\n", size); */
	dump_image(as, nascii, frameBuffer);
	clUpdateTail(frameBufferHdl, size);
    }
  done:

#if 0
    clCreateBuf(decompressorHdl, CL_BUF_COMPRESSED,
		compressedBufferSize, 1, NULL);
    /* Decompress a series of frames */
    for(i = 0; i < N; i++)
    {
	/* Keep input buffer full with compressed data */
               ...
              clDecompress(handle, 1, 0, NULL, frameBuffer);
	       /* Write the frame to somewhere (such as the screen) */
               ...
	   }
#endif

    /* Close the decompressor */
    clCloseDecompressor(decompressorHdl);
    close(inFile);

#ifdef DUMP_CHAR
    close_display();
#endif /* DUMP_CHAR */

    exit(0);
}
