/* -*- Mode: C; tab-width: 4 -*-
 * lightning --- fractal lightning bolts.
 */
#if !defined( lint ) && !defined( SABER )
static const char sccsid[] = "@(#)lightning.c	4.00 97/01/01 xlockmore";
#endif

/* Copyright (c) 1996 by Keith Romberg <kromberg@saxe.com>.
 *
 * Permission to use, copy, modify, and distribute this software and its
 * documentation for any purpose and without fee is hereby granted,
 * provided that the above copyright notice appear in all copies and that
 * both that copyright notice and this permission notice appear in
 * supporting documentation.
 *
 * This file is provided AS IS with no warranties of any kind.  The author
 * shall have no liability with respect to the infringement of copyrights,
 * trade secrets or any patents by this file or any part thereof.  In no
 * event will the author be liable for any lost revenue or profits or
 * other special, indirect and consequential damages.
 *
 * Revision History:
 * 10-May-97: jwz@jwz.org: turned into a standalone program.
 * 14-Jul-96: Cleaned up code.
 * 27-Jun-96: Written and submitted by Keith Romberg <kromberg@saxe.com>.
 */

#ifdef STANDALONE
# define PROGCLASS					"Lightning"
# define HACK_INIT					init_lightning
# define HACK_DRAW					draw_lightning
# define lightning_opts				xlockmore_opts
# define DEFAULTS	"*delay:		10000 \n"			\
					"*ncolors:		200   \n"
# define BRIGHT_COLORS
# include "xlockmore.h"				/* from the xscreensaver distribution */
#else  /* !STANDALONE */
# include "xlock.h"					/* from the xlockmore distribution */
#endif /* !STANDALONE */

ModeSpecOpt lightning_opts = {
  0, NULL, 0, NULL, NULL };

/*----------------------------   defines   -------------------------------*/

#define BOLT_NUMBER 4
#define BOLT_ITERATION 4
#define LONG_FORK_ITERATION 3
#define MEDIUM_FORK_ITERATION 2
#define SMALL_FORK_ITERATION 1

#define WIDTH_VARIATION 30
#define HEIGHT_VARIATION 15

#define DELAY_TIME_AMOUNT 15
#define MULTI_DELAY_TIME_BASE 5

#define MAX_WIGGLES 16
#define WIGGLE_BASE 8
#define WIGGLE_AMOUNT 14

#define RANDOM_FORK_PROBILITY   4

#define FIRST_LEVEL_STRIKE 0
#define LEVEL_ONE_STRIKE 1
#define LEVEL_TWO_STRIKE 2

#define BOLT_VERTICIES ((1<<BOLT_ITERATION)-1)
  /* BOLT_ITERATION = 4. 2^(BOLT_ITERATION) - 1 = 15 */

#define NUMBER_FORK_VERTICIES 9

#define FLASH_PROBILITY 20
#define MAX_FLASH_AMOUNT 2	/*  half the total duration of the bolt  */

typedef struct {
	XPoint      ForkVerticies[NUMBER_FORK_VERTICIES];
	int         num_used;
} Fork;

typedef struct {
	XPoint      end1, end2;
	XPoint      middle[BOLT_VERTICIES];
	int         fork_number;
	int         forks_start[2];
	Fork        branch[2];
	int         wiggle_number;
	int         wiggle_amount;
	int         delay_time;
	int         flash;
	int         flash_begin, flash_stop;
	int         visible;
	int         strike_level;
} Lightning;

typedef struct {
	Lightning   bolts[BOLT_NUMBER];
	int         scr_width, scr_height;
	int         multi_strike;
	int         give_it_hell;
	int         draw_time;
	int         stage;
	unsigned long color;
} Storm;

static Storm *Helga = NULL;

/*-------------------   function prototypes  ----------------------------*/

static int  distance(XPoint a, XPoint b);

static int  setup_multi_strike(void);
static int  flashing_strike(void);
static void flash_duration(int *start, int *end, int total_duration);
static void random_storm(Storm * st);
static void generate(XPoint A, XPoint B, int iter, XPoint * verts, int *index);
static void create_fork(Fork * f, XPoint start, XPoint end, int level);

static void first_strike(Lightning bolt, ModeInfo * mi);
static void draw_bolt(Lightning * bolt, ModeInfo * mi);
static void draw_line(ModeInfo * mi, XPoint * p, int number, GC use, int x_offset);
static void level1_strike(Lightning bolt, ModeInfo * mi);
static void level2_strike(Lightning bolt, ModeInfo * mi);

static int  storm_active(Storm * st);
static void update_bolt(Lightning * bolt, int time);
static void wiggle_bolt(Lightning * bolt);
static void wiggle_line(XPoint * p, int number, int wiggle_amount);

/*-------------------------  functions  ---------------------------------*/

static int
setup_multi_strike(void)
{
	int         result, multi_prob;

	multi_prob = NRAND(100);

	if (multi_prob < 50)
		result = 1;
	else if ((multi_prob >= 51) && (multi_prob < 75))
		result = 2;
	else if ((multi_prob >= 76) && (multi_prob < 92))
		result = 3;
	else
		result = BOLT_NUMBER;	/* 4 */

	return (result);
}

/*-------------------------------------------------------------------------*/

static int
flashing_strike(void)
{
	int         tmp = NRAND(FLASH_PROBILITY);

	if (tmp <= FLASH_PROBILITY)
		return (1);
	return (0);
}

/*-------------------------------------------------------------------------*/

static void
flash_duration(int *start, int *end, int total_duration)
{
	int         mid, d;

	mid = total_duration / MAX_FLASH_AMOUNT;
	d = NRAND(total_duration / MAX_FLASH_AMOUNT) / 2;
	*start = mid - d;
	*end = mid + d;
}

/*-------------------------------------------------------------------------*/

static void
random_storm(Storm * st)
{
	int         i, j, tmp;
	XPoint      p;

	for (i = 0; i < st->multi_strike; i++) {
		st->bolts[i].end1.x = NRAND(st->scr_width);
		st->bolts[i].end1.y = 0;
		st->bolts[i].end2.x = NRAND(st->scr_width);
		st->bolts[i].end2.y = st->scr_height;
		st->bolts[i].wiggle_number = WIGGLE_BASE + NRAND(MAX_WIGGLES);
		if ((st->bolts[i].flash = flashing_strike()))
			flash_duration(&(st->bolts[i].flash_begin), &(st->bolts[i].flash_stop),
				       st->bolts[i].wiggle_number);
		else
			st->bolts[i].flash_begin = st->bolts[i].flash_stop = 0;
		st->bolts[i].wiggle_amount = WIGGLE_AMOUNT;
		if (i == 0)
			st->bolts[i].delay_time = NRAND(DELAY_TIME_AMOUNT);
		else
			st->bolts[i].delay_time = NRAND(DELAY_TIME_AMOUNT) +
				(MULTI_DELAY_TIME_BASE * i);
		st->bolts[i].strike_level = FIRST_LEVEL_STRIKE;
		tmp = 0;
		generate(st->bolts[i].end1, st->bolts[i].end2, BOLT_ITERATION,
			 st->bolts[i].middle, &tmp);
		st->bolts[i].fork_number = 0;
		st->bolts[i].visible = 0;
		for (j = 0; j < BOLT_VERTICIES; j++) {
			if (st->bolts[i].fork_number >= 2)
				break;
			if (NRAND(100) < RANDOM_FORK_PROBILITY) {
				p.x = NRAND(st->scr_width);
				p.y = st->scr_height;
				st->bolts[i].forks_start[st->bolts[i].fork_number] = j;
				create_fork(&(st->bolts[i].branch[st->bolts[i].fork_number]),
					    st->bolts[i].middle[j], p, j);
				st->bolts[i].fork_number++;
			}
		}
	}
}

static void
generate(XPoint A, XPoint B, int iter, XPoint * verts, int *index)
{
	XPoint      mid;

	mid.x = (A.x + B.x) / 2 + NRAND(WIDTH_VARIATION) - WIDTH_VARIATION / 2;
	mid.y = (A.y + B.y) / 2 + NRAND(HEIGHT_VARIATION) - HEIGHT_VARIATION / 2;

	if (!iter) {
		verts[*index].x = mid.x;
		verts[*index].y = mid.y;
		(*index)++;
		return;
	}
	generate(A, mid, iter - 1, verts, index);
	generate(mid, B, iter - 1, verts, index);
}

/*------------------------------------------------------------------------*/

static void
create_fork(Fork * f, XPoint start, XPoint end, int level)
{
	int         tmp = 1;

	f->ForkVerticies[0].x = start.x;
	f->ForkVerticies[0].y = start.y;

	if (level <= 6) {
		generate(start, end, LONG_FORK_ITERATION, f->ForkVerticies, &tmp);
		f->num_used = 9;
	} else if ((level > 6) && (level <= 11)) {
		generate(start, end, MEDIUM_FORK_ITERATION, f->ForkVerticies, &tmp);
		f->num_used = 5;
	} else {
		if (distance(start, end) > 100) {
			generate(start, end, MEDIUM_FORK_ITERATION, f->ForkVerticies, &tmp);
			f->num_used = 5;
		} else {
			generate(start, end, SMALL_FORK_ITERATION, f->ForkVerticies, &tmp);
			f->num_used = 3;
		}
	}

	f->ForkVerticies[f->num_used - 1].x = end.x;
	f->ForkVerticies[f->num_used - 1].y = end.y;
}

/*------------------------------------------------------------------------*/

static void
update_bolt(Lightning * bolt, int time)
{
	wiggle_bolt(bolt);
	if ((bolt->wiggle_amount == 0) && (bolt->wiggle_number > 2))
		bolt->wiggle_number = 0;
	if (((time % 3) == 0))
		bolt->wiggle_amount++;

	if (((time >= bolt->delay_time) && (time < bolt->flash_begin)) ||
	    (time > bolt->flash_stop))
		bolt->visible = 1;
	else
		bolt->visible = 0;

	if (time == bolt->delay_time)
		bolt->strike_level = FIRST_LEVEL_STRIKE;
	else if (time == (bolt->delay_time + 1))
		bolt->strike_level = LEVEL_ONE_STRIKE;
	else if ((time > (bolt->delay_time + 1)) &&
		 (time <= (bolt->delay_time + bolt->flash_begin - 2)))
		bolt->strike_level = LEVEL_TWO_STRIKE;
	else if (time == (bolt->delay_time + bolt->flash_begin - 1))
		bolt->strike_level = LEVEL_ONE_STRIKE;
	else if (time == (bolt->delay_time + bolt->flash_stop + 1))
		bolt->strike_level = LEVEL_ONE_STRIKE;
	else
		bolt->strike_level = LEVEL_TWO_STRIKE;
}

/*------------------------------------------------------------------------*/

static void
draw_bolt(Lightning * bolt, ModeInfo * mi)
{
	if (bolt->visible) {
		if (bolt->strike_level == FIRST_LEVEL_STRIKE)
			first_strike(*bolt, mi);
		else if (bolt->strike_level == LEVEL_ONE_STRIKE)
			level1_strike(*bolt, mi);
		else
			level2_strike(*bolt, mi);
	}
}

/*------------------------------------------------------------------------*/

static void
first_strike(Lightning bolt, ModeInfo * mi)
{
	Display    *display = MI_DISPLAY(mi);
	Window      window = MI_WINDOW(mi);
	GC          gc = MI_GC(mi);
	int         i;

	XSetForeground(display, gc, MI_WIN_WHITE_PIXEL(mi));
	XDrawLine(display, window, gc,
	       bolt.end1.x, bolt.end1.y, bolt.middle[0].x, bolt.middle[0].y);
	draw_line(mi, bolt.middle, BOLT_VERTICIES, gc, 0);
	XDrawLine(display, window, gc,
	bolt.middle[BOLT_VERTICIES - 1].x, bolt.middle[BOLT_VERTICIES - 1].y,
		  bolt.end2.x, bolt.end2.y);

	for (i = 0; i < bolt.fork_number; i++)
		draw_line(mi, bolt.branch[i].ForkVerticies, bolt.branch[i].num_used,
			  gc, 0);
}

/*------------------------------------------------------------------------*/

static void
draw_line(ModeInfo * mi, XPoint * points, int number, GC to_use, int offset)
{
	int         i;

	for (i = 0; i < number - 1; i++) {
		if (points[i].y <= points[i + 1].y)
			XDrawLine(MI_DISPLAY(mi), MI_WINDOW(mi), to_use, points[i].x + offset,
				  points[i].y, points[i + 1].x + offset, points[i + 1].y);
		else {
			if (points[i].x < points[i + 1].x)
				XDrawLine(MI_DISPLAY(mi), MI_WINDOW(mi), to_use, points[i].x +
					  offset, points[i].y + offset, points[i + 1].x + offset,
					  points[i + 1].y + offset);
			else
				XDrawLine(MI_DISPLAY(mi), MI_WINDOW(mi), to_use, points[i].x -
					  offset, points[i].y + offset, points[i + 1].x - offset,
					  points[i + 1].y + offset);
		}
	}
}

/*------------------------------------------------------------------------*/

static void
level1_strike(Lightning bolt, ModeInfo * mi)
{
	Display    *display = MI_DISPLAY(mi);
	Window      window = MI_WINDOW(mi);
	Storm      *st = &Helga[MI_SCREEN(mi)];
	GC          gc = MI_GC(mi);
	int         i;

	if (MI_NPIXELS(mi) > 2)	/* color */
		XSetForeground(display, gc, MI_PIXEL(mi, st->color));
	else
		XSetForeground(display, gc, MI_WIN_WHITE_PIXEL(mi));
	XDrawLine(display, window, gc,
	bolt.end1.x - 1, bolt.end1.y, bolt.middle[0].x - 1, bolt.middle[0].y);
	draw_line(mi, bolt.middle, BOLT_VERTICIES, gc, -1);
	XDrawLine(display, window, gc,
		  bolt.middle[BOLT_VERTICIES - 1].x - 1,
	    bolt.middle[BOLT_VERTICIES - 1].y, bolt.end2.x - 1, bolt.end2.y);
	XDrawLine(display, window, gc,
	bolt.end1.x + 1, bolt.end1.y, bolt.middle[0].x + 1, bolt.middle[0].y);
	draw_line(mi, bolt.middle, BOLT_VERTICIES, gc, 1);
	XDrawLine(display, window, gc,
		  bolt.middle[BOLT_VERTICIES - 1].x + 1,
	    bolt.middle[BOLT_VERTICIES - 1].y, bolt.end2.x + 1, bolt.end2.y);

	for (i = 0; i < bolt.fork_number; i++) {
		draw_line(mi, bolt.branch[i].ForkVerticies, bolt.branch[i].num_used,
			  gc, -1);
		draw_line(mi, bolt.branch[i].ForkVerticies, bolt.branch[i].num_used,
			  gc, 1);
	}
	first_strike(bolt, mi);
}

/*------------------------------------------------------------------------*/

static int
distance(XPoint a, XPoint b)
{
	return ((int) sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y)));
}

/*------------------------------------------------------------------------*/

static void
level2_strike(Lightning bolt, ModeInfo * mi)
{
	Display    *display = MI_DISPLAY(mi);
	Window      window = MI_WINDOW(mi);
	Storm      *st = &Helga[MI_SCREEN(mi)];
	GC          gc = MI_GC(mi);
	int         i;

	/* This was originally designed to be a little darker then the
	   level1 strike.  This was changed to get it to work on
	   multiscreens and to add more color variety.   I tried
	   stippling but it did not look good. */
	if (MI_NPIXELS(mi) > 2)
		XSetForeground(display, gc, MI_PIXEL(mi, st->color));
	else
		XSetForeground(display, gc, MI_WIN_WHITE_PIXEL(mi));
	XDrawLine(display, window, gc,
	bolt.end1.x - 2, bolt.end1.y, bolt.middle[0].x - 2, bolt.middle[0].y);
	draw_line(mi, bolt.middle, BOLT_VERTICIES, gc, -2);
	XDrawLine(display, window, gc,
		  bolt.middle[BOLT_VERTICIES - 1].x - 2,
	    bolt.middle[BOLT_VERTICIES - 1].y, bolt.end2.x - 2, bolt.end2.y);

	XDrawLine(display, window, gc,
	bolt.end1.x + 2, bolt.end1.y, bolt.middle[0].x + 2, bolt.middle[0].y);
	draw_line(mi, bolt.middle, BOLT_VERTICIES, gc, 2);
	XDrawLine(display, window, gc,
		  bolt.middle[BOLT_VERTICIES - 1].x + 2,
	    bolt.middle[BOLT_VERTICIES - 1].y, bolt.end2.x + 2, bolt.end2.y);

	for (i = 0; i < bolt.fork_number; i++) {
		draw_line(mi, bolt.branch[i].ForkVerticies, bolt.branch[i].num_used,
			  gc, -2);
		draw_line(mi, bolt.branch[i].ForkVerticies, bolt.branch[i].num_used,
			  gc, 2);
	}
	level1_strike(bolt, mi);
}

/*------------------------------------------------------------------------*/

static int
storm_active(Storm * st)
{
	int         i, atleast_1 = 0;

	for (i = 0; i < st->multi_strike; i++)
		if (st->bolts[i].wiggle_number > 0)
			atleast_1++;

	return (atleast_1);
}

/*------------------------------------------------------------------------*/

static void
wiggle_bolt(Lightning * bolt)
{
	int         i;

	wiggle_line(bolt->middle, BOLT_VERTICIES, bolt->wiggle_amount);
	bolt->end2.x += NRAND(bolt->wiggle_amount) - bolt->wiggle_amount / 2;
	bolt->end2.y += NRAND(bolt->wiggle_amount) - bolt->wiggle_amount / 2;

	for (i = 0; i < bolt->fork_number; i++) {
		wiggle_line(bolt->branch[i].ForkVerticies, bolt->branch[i].num_used,
			    bolt->wiggle_amount);
		bolt->branch[i].ForkVerticies[0].x = bolt->middle[bolt->forks_start[i]].x;
		bolt->branch[i].ForkVerticies[0].y = bolt->middle[bolt->forks_start[i]].y;
	}

	if (bolt->wiggle_amount > 1)
		bolt->wiggle_amount -= 1;
	else
		bolt->wiggle_amount = 0;
}

/*------------------------------------------------------------------------*/

static void
wiggle_line(XPoint * p, int number, int amount)
{
	int         i;

	for (i = 0; i < number; i++) {
		p[i].x += NRAND(amount) - amount / 2;
		p[i].y += NRAND(amount) - amount / 2;
	}
}

/*------------------------------------------------------------------------*/

void
init_lightning(ModeInfo * mi)
{
	Storm      *st;

	if (Helga == NULL) {
		if ((Helga = (Storm *) calloc(MI_NUM_SCREENS(mi),
					      sizeof (Storm))) == NULL)
			return;
	}
	st = &Helga[MI_SCREEN(mi)];

	st->scr_width = MI_WIN_WIDTH(mi);
	st->scr_height = MI_WIN_HEIGHT(mi);

	st->multi_strike = setup_multi_strike();
	random_storm(st);
	st->stage = 0;
}

/*------------------------------------------------------------------------*/

void
draw_lightning(ModeInfo * mi)
{
	Storm      *st = &Helga[MI_SCREEN(mi)];
	int         i;

	switch (st->stage) {
		case 0:
			XClearWindow(MI_DISPLAY(mi), MI_WINDOW(mi));
			st->color = NRAND(MI_NPIXELS(mi));
			st->draw_time = 0;
			if (storm_active(st))
				st->stage++;
			else
				st->stage = 3;
			break;
		case 1:
			for (i = 0; i < st->multi_strike; i++) {
				if (st->bolts[i].visible)
					draw_bolt(&(st->bolts[i]), mi);
				update_bolt(&(st->bolts[i]), st->draw_time);
			}
			st->draw_time++;
			XFlush(MI_DISPLAY(mi));
			MI_PAUSE(mi) = 60000;
			st->stage++;
			break;
		case 2:
			XClearWindow(MI_DISPLAY(mi), MI_WINDOW(mi));
			if (storm_active(st))
				st->stage = 1;
			else
				st->stage++;
			break;
		case 3:
			MI_PAUSE(mi) = 1000000;
			init_lightning(mi);
			break;
	}
}

void
release_lightning(ModeInfo * mi)
{
	if (Helga != NULL) {
		(void) free((void *) Helga);
		Helga = NULL;
	}
}

void
refresh_lightning(ModeInfo * mi)
{
	/* Do nothing, it will refresh by itself */
}
