/* starfish.c
 * version 0.2
 *
 * Copyright (C) 2004, Andy Goth <unununium@openverse.com>
 * http://ioioio.net/
 *
 * This crap, I mean program, is available under the GNU General Public
 * License, and therefore has no warranty.  (As if anyone would ever want to
 * reuse my code!)
 *
 * This program only works in the Linux console.  And when I say console, I
 * mean *text console*.  It might work in the framebuffer console, but it'll be
 * slow (actually it's slow either way).  Things probably look best in 80x25.
 * When you see it running, remind yourself that you are still in text mode.
 *
 * $ gcc -o starfish starfish.c -lm -lcurses
 * $ ./starfish
 *
 * To quit the program, press any key.  I recommend you do it quickly, before
 * you fall in and are lost forever.  And did I mention there's no warranty!?
 *
 * Whatever you do, don't try to run it in the background or change virtual
 * consoles (unless you're using fbcon).  The Linux VGA console really should
 * add support for different fonts for each virtual console, like fbcon does...
 *
 * This program would be a lot faster if it had direct access to VRAM, but as
 * it is, it doesn't even require root access.  Hooray for that.  If the Linux
 * console added some escape sequences to change the VGA font, this program
 * could run over telnet/ssh.  Frightening.
 *
 * My code has a bitmapped image in mind that it wants to display, but it must
 * convert it into a (funky) character-cell pseudo-ASCII-art grid (using
 * UTF-8 to encode control characters, no less!), which in turn must be
 * converted into terminal-control escape sequences to pass to the kernel,
 * which then converts from escape sequences to a character-cell grid which it
 * hands off to the VGA, which converts from fonts and characters into a
 * bitmapped image.  A more direct approach would be nice... :^)
 *
 * So, in search of said direct approach, I tried using /dev/vcsa*, which
 * allows me to write raw character and attribute tothe screen using lseek()
 * and write(), but amazingly enough I actually got better performance with
 * the old code, even though it added four more levels of translation
 * (character code to UTF-8, color code to SGR; UTF-8 to character code, SGR to
 * color code).  The kernel vcsa driver probably needs optimization. */

#define __KERNEL_STRICT_NAMES
#include <errno.h>
#include <fcntl.h>
#include <linux/kd.h>
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <term.h>
#include <termios.h>
#include <unistd.h>

/* Change this to 8 or 9 as appropriate, or if you're using some horrid laptop
 * screen with fixed pixels, use 0. */
#define FONT_WIDTH 9

#if FONT_WIDTH == 8

/* 640x400 screen.       */
#define ASPECT ( 5. / 12.)

#elif FONT_WIDTH == 9

/* 720x400 screen.       */
#define ASPECT (10. / 27.)

#else

/* Assuming square dots. */
#define ASPECT ( 1. /  2.)

#endif

static struct console_font_op old_font;
static struct termios         old_term;

static void cleanup(int sig)
{
    /* Restore font. */
    if (old_font.data != NULL) {
        old_font.op = KD_FONT_OP_SET;
        if (ioctl(1, KDFONTOP, &old_font) == -1) {
            perror("ioctl");
        }
        free(old_font.data);
    }

    /* Restore terminal. */
    if (isatty(1) && tcsetattr(1, TCSAFLUSH, &old_term) == -1) {
        perror("tcsetattr");
    }

    /* Disable UTF-8. */
    printf("\e%%@");

    /* Clear screen. */
    printf("\e[0m\e[2J\e[1;1H");

    /* Prepare to evacuate soul... */
    exit(sig == -1 ? EXIT_FAILURE : EXIT_SUCCESS);
}

int main(int argc, char** argv)
{
    int i, j, y, x;
    int xc, yc, h, a, d;
    int rows, cols;
    struct console_font_op new_font;
    struct termios         new_term;
    unsigned char chr, attr, prev_attr;

    if (isatty(1)) {
        /* Get old terminal settings. */
        if (tcgetattr(1, &old_term) == -1) {
            perror("tcgetattr");
            return EXIT_FAILURE;
        }

        /* Set raw mode (all I really need is to disable the line buffer). */
        new_term = old_term;
        cfmakeraw(&new_term);
        if (tcsetattr(1, TCSANOW, &new_term) == -1) {
            perror("tcsetattr");
            cleanup(-1);
        }

        /* Get old font. */
        old_font.op        = KD_FONT_OP_GET;
        old_font.flags     = 0;
        old_font.data      = malloc(512 * 32);
        old_font.width     = 8;
        old_font.height    = 32;
        old_font.charcount = 512;
        if (ioctl(1, KDFONTOP, &old_font) == -1) {
            perror("ioctl");
            free(old_font.data);
            cleanup(-1);
        }

        /* Set new font. */
        new_font.op        = KD_FONT_OP_SET;
        new_font.flags     = 0;
        new_font.data      = malloc(256 * 32);
        new_font.width     = 8;
        new_font.height    = 2;
        new_font.charcount = 256;
        for (i = 0; i < 256; ++i) {
            new_font.data[i * 32 + 0] = new_font.data[i * 32 + 1] = (char)
                    (((i & 128) >> 7) | ((i &  64) >> 5) | ((i &  32) >> 3) |
                     ((i &  16) >> 1) | ((i &   8) << 1) | ((i &   4) << 3) |
                     ((i &   2) << 5) | ((i &   1) << 7));
        }
        if (ioctl(1, KDFONTOP, &new_font) == -1) {
            perror("ioctl");
            if (tcsetattr(0, TCSANOW, &old_term) == -1) {
                perror("tcsetattr");
            }
            return EXIT_FAILURE;
        }
        free(new_font.data);
    } else {
        /* Redirecting to a file, for some weird reason... */
        old_font.data = NULL;
    }

    /* Arrange to restore sanity to the terminal. */
    signal(SIGINT , cleanup);
    signal(SIGTERM, cleanup);
    signal(SIGIO  , cleanup);

    /* Configure stdin to use SIGIO. */
    fcntl(0, F_SETOWN, getpid());
    fcntl(0, F_SETFL, fcntl(0, F_GETFL) | O_ASYNC | O_NONBLOCK);

    /* Get screen size. */
    rows = tigetnum("lines"); if (rows <= 0) rows = 200;
    cols = tigetnum("cols" ); if (cols <= 0) cols =  80;

    /* Enable UTF-8. */
    printf("\e%%G");

    /* Main loop. */
    prev_attr = 0xff;
    for (j = 0;; ++j) {
        /* Start at the upper-lefthand corner of the screen. */
        printf("\e[1;1H");

        /* Screen parameters (assuming 80x25):
         *
         * - Resolution, dots       : 640x400 (720x400)
         * - Resolution, pixels     : 640x200
         * - Resolution, characters :  80x200
         * - Character cell, dots   :   8x2   (  9x2  )
         * - Character cell, pixels :   8x1
         * - Dots per pixel (WxH)   :   1x2
         * - Pixel aspect (W:H)     :   5:12  ( 10:27 )
         * - Screen size, bytes     :  32000 = 80 * 200 * 2
         * - Total VRAM, bytes      :  32768 = b8000 through bffff
         *
         * Above, a pixel is the smallest individually-addressable screen
         * element in this funky screen hack, and a dot is the smallest point
         * addressable by the VGA (by changing the font).  Each character has a
         * foreground and a background attribute, and all pixels within a
         * character must be one or the other.  Normally there are sixteen
         * foreground and eight background attributes (which, by the way, can
         * be remapped if you really want), and if you disable blinking (I wish
         * the Linux console would add support for this) then you can get
         * sixteen background attributes.
         *
         * For best results, use 8-pixel-wide fonts.  The VGA usually uses
         * 9-pixel-wide fonts, resulting in those dark vertical lines.
         * SVGATextMode is one way of making this adjustment.  Personally, I
         * simply hacked up arch/i386/boot/video.S. :^) */

        for (y = 0; y < rows; ++y) {         /* For each pixel row... */
            for (x = 0; x < cols; ++x) {     /* For each character... */
                /* Build the character. */
                chr = 0;
                for (i = 0; i < 8; ++i) {    /* For each pixel...     */
                    xc = (x * 8 + i - cols * 4) * ASPECT; /* X coordinate.   */
                    yc = y - rows / 2;                    /* Y coordinate.   */
                    h = hypot(xc, yc);                    /* Radius (hypot). */
                    a = atan2(xc, yc) * 180 / M_PI;       /* Angle (arctan). */

                    if ((h + j + a + 180) % 10 < 5) {
                        /* Create a spiral going one direction.  This spiral is
                         * composed of foreground and background *pixels*. */
                        chr |= 1 << i;
                    }
                }

                /* Build the attribute.  Create psychedelic starfish. :^)  The
                 * starfish determines whether the secondary spiral is drawn in
                 * red, magenta, or blue, and the secondary spiral determines
                 * what colors are used to draw the primary spiral.  It's all
                 * connected, man! */
                d = (int)(h + j + sin((j - a) * M_PI / 36) * 10 + 10) % 50;
                if      (d < 20) d = 0;
                else if (d < 25) d = 1;
                else if (d < 45) d = 2;
                else if (d < 50) d = 1;

                /* Create a spiral going the other direction.  This
                 * spiral is composed of foreground and background
                 * *attributes*, determining the color of the pixels. */
                if ((h - j - a - 360) % 10 > -5) {
                    /* Bright colors. */
                    switch (d) {
                    case 0: attr = 0x4c; break; /* Blue.    */
                    case 1: attr = 0x5d; break; /* Magenta. */
                    case 2: attr = 0x19; break; /* Red.     */
                    }
                } else {
                    /* Dark colors. */
                    switch (d) {
                    case 0: attr = 0x04; break; /* Blue.    */
                    case 1: attr = 0x05; break; /* Magenta. */
                    case 2: attr = 0x01; break; /* Red.     */
                    }
                }

                if (attr != prev_attr) {
                    /* Change colors. */
                    printf("\e[%d;3%d;4%dm", (attr >> 3) & 1,
                            (attr >> 0) & 7, (attr >> 4) & 7);
                    prev_attr = attr;
                }

                /* Display the character. */
                if (chr < 32 || chr > 126) {
                    /* UTF-8 direct-to-font. */
                    putchar(0xef);
                    putchar(0x80 | ((chr >> 6) &  3));
                    putchar(0x80 | ((chr >> 0) & 63));
                } else {
                    /* ASCII. */
                    putchar(chr);
                }
            }
        }
    }
}

/* vim: set ts=4 sts=4 sw=4 et: */

