/* FUBK test image generator
 *
 * (C) 2017 by Andreas Eversberg <jolly@eversberg.eu>
 * All Rights Reserved
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <math.h>
#include <stdlib.h>
#include <stdint.h>
#include "../libsample/sample.h"
#include "fubk.h"
#include "font.h"

#define GRID_LINES	4
#define GRID_HEIGHT	40
#define CENTER_LINE	(287 - 2)

#define GRID_WIDTH	0.0000027
#define RAMP_WIDTH	0.00000015
#define TEXT_WIDTH	0.0000002

#define GRID_LEVEL	1.0
#define FIELD_LEVEL	0.25

#define CIRCLE_LEVEL	1.0
#define CIRCLE_CENTER	(287 - 1)
#define CIRCLE_HEIGTH	3

static struct color_bar {
	double amplitude, phase;
} color_bar[8] = {
	{0.0, 0.0},
	{0.336, 167.1},
	{0.474, 283.5},
	{0.443, 240.7},
	{0.443, 60.7},
	{0.474, 103.5},
	{0.336, 347.1},
	{0.0, 0.0},
};

static struct multi_burst {
	double width;		/* how whide is this portion */
	double level;		/* level of this portion */
	double frequency;	/* frequency of burst or zero */
	double V;		/* amplitude of V */
} multi_burst[10] = {
	{ 3.9, 1.0, 0.0, 0.0 },		/* white */
	{ 0.7, 0.5, 0.0, 0.0 },
	{ 5.0, 0.5, 1000000.0, 0.0 },	/* 1 MHz */
	{ 0.7, 0.5, 0.0, 0.0 },
	{ 5.0, 0.5, 2000000.0, 0.0 },	/* 2 MHz */
	{ 2.0, 0.5, 0.0, 0.0 },
	{ 5.0, 0.5, 3000000.0, 0.0 },	/* 3 MHz */
	{ 0.7, 0.5, 0.0, 0.0 },
	{ 7.9, 0.5, 0.0, 0.5 },		/* Color burst */
	{ 1.5, 0.5, 0.0, 0.0 },
	/* note that the color burst ist shifted left by 0.4uS when delaying Y.
	 * this makes color burst appear left of the center.
	 * we don't compensate this here, since i have seen this shift in real generators. */
};

/* create cosine ramp, so that the bandwidth is not higher than 2 * width of the ramp(0..1) */
static inline double ramp(double x)
{
	return 0.5 - 0.5 * cos(x * M_PI);
}

static double mittelfeld(sample_t *sample, double samplerate, int *_i, double *_x, double render_start, double step, sample_t *color_u, sample_t *color_v, int v_polarity, int line, const char *station_id)
{
	double render_end, x = *_x, vline_x = *_x, vline_start = render_start;
	int i = *_i, vline_i = *_i, b;
	double amplitude, phase, phase_step, Y, U, V, colorphase;
	int position = line / GRID_HEIGHT;
	int char_line;
	uint8_t bits;
	int bit, last_bit, c;

	switch (position) {
	case 0:
	case 1:
	case 2:
		/* color bars */
		for (b = 0; b < 8; b++) {
			Y = (1.0 - (double)b / 7.0) * 0.75;
			amplitude = color_bar[b].amplitude;
			if (v_polarity < 0)
				colorphase = (360.0 - color_bar[b].phase) / 180.0 * M_PI;
			else
				colorphase = color_bar[b].phase / 180.0 * M_PI;
			U = cos(colorphase) * amplitude;
			V = sin(colorphase) * amplitude;
			render_end = render_start + GRID_WIDTH * 1.5;
			while (x < render_end) {
				color_u[i] = U;
				color_v[i] = V;
				sample[i++] = Y;
				x += step;
			}
			render_start = render_end;
		}
		break;
	case 3:
	case 4:
		/* gray steps */
		for (b = 0; b < 5; b++) {
			Y = (double)b / 4.0;
			render_end = render_start + GRID_WIDTH * 2.4;
			while (x < render_end) {
				sample[i++] = Y;
				x += step;
			}
			render_start = render_end;
		}
		break;
	case 5:
		/* station ID */
		/* white bar before text */
		render_end = render_start + GRID_WIDTH * 2.4;
		while (x < render_end) {
			sample[i++] = 1.0;
			x += step;
		}
		render_start = render_end;
		/* black bar before text (exclude one ramp width) */
		render_end = render_start + GRID_WIDTH * 2.4 / 54.0 - RAMP_WIDTH / 2.0;
		while (x < render_end) {
			sample[i++] = 0.0;
			x += step;
		}
		render_start = render_end;
		last_bit = 0;
		/* text */
		/* 12 chars, plus one extra bit */
		for (c = 0; c < 13; c++) {
			char_line = ((line % GRID_HEIGHT) + 1) / 4 - 2;
			if (char_line < 0 || char_line > 7 || c == 12)
				bits = 0x00;
			else
				bits = get_font(station_id[c])[char_line];
			for (b = 0; b < 8; b++) {
				bit = ((bits << b) & 128);
				if (!last_bit && !bit) {
					/* keep black */
					render_end = render_start + TEXT_WIDTH;
					while (x < render_end) {
						sample[i++] = 0.0;
						x += step;
					}
					render_start = render_end;
				}
				if (last_bit && bit) {
					/* keep white */
					render_end = render_start + TEXT_WIDTH;
					while (x < render_end) {
						sample[i++] = 1.0;
						x += step;
					}
					render_start = render_end;
				}
				if (!last_bit && bit) {
					/* ramp to white */
					render_end = render_start + TEXT_WIDTH;
					while (x < render_end) {
						sample[i++] = 1.0 - ramp((render_end - x) / TEXT_WIDTH);
						x += step;
					}
					render_start = render_end;
				}
				if (last_bit && !bit) {
					/* ramp to black */
					render_end = render_start + TEXT_WIDTH;
					while (x < render_end) {
						sample[i++] = ramp((render_end - x) / TEXT_WIDTH);
						x += step;
					}
					render_start = render_end;
				}
				last_bit = bit;
				/* after extra bit after 12 chars, stop rendering */
				if (c == 12)
					break;
			}
		}
		/* black bar after text */
		render_end = render_start + GRID_WIDTH * 2.4 / 54.0 - RAMP_WIDTH / 2.0;
		while (x < render_end) {
			sample[i++] = 0.0;
			x += step;
		}
		render_start = render_end;
		/* white bar after text */
		render_end = render_start + GRID_WIDTH * 2.4;
		while (x < render_end) {
			sample[i++] = 1.0;
			x += step;
		}
		render_start = render_end;
		break;
	case 6:
		/* multi-burst */
		for (b = 0; b < 10; b++) {
			Y = multi_burst[b].level;
			V = multi_burst[b].V;
			phase_step = multi_burst[b].frequency * 2.0 * M_PI / samplerate;
			phase = 0.0;
			render_end = render_start + multi_burst[b].width / 1e6;
			while (x < render_end) {
				if (v_polarity < 0)
					colorphase = (360.0 - 145.9) / 180.0 * M_PI;
				else
					colorphase = 145.9 / 180.0 * M_PI;
				color_u[i] = cos(colorphase) * V;
				color_v[i] = sin(colorphase) * V;
				sample[i++] = Y + sin(phase) / 2.0;
				phase += phase_step;
				x += step;
			}
			render_start = render_end;
		}
		break;
	case 7:
		/* white bar with black pulse triangle */
		/* white bar */
		render_end = render_start + GRID_WIDTH * 6 - 0.0000005 - RAMP_WIDTH;
		while (x < render_end) {
			sample[i++] = 1.0;
			x += step;
		}
		/* ramp to triangle */
		render_end += RAMP_WIDTH;
		while (x < render_end) {
			sample[i++] = ramp((render_end - x) / RAMP_WIDTH);
			x += step;
		}
		/* triangle */
		render_end += 0.000001 * (1.0 - ((double)(line % GRID_HEIGHT) / (double)GRID_HEIGHT));
		while (x < render_end) {
			sample[i++] = 0.0;
			x += step;
		}
		/* ramp from triangle */
		render_end += RAMP_WIDTH;
		while (x < render_end) {
			sample[i++] = 1.0 - ramp((render_end - x) / RAMP_WIDTH);
			x += step;
		}
		/* white bar */
		render_end = render_start + GRID_WIDTH * 12;
		while (x < render_end) {
			sample[i++] = 1.0;
			x += step;
		}
		render_start = render_end;
		break;
	case 8:
		/* sawtooth +-V and uncolored field */
		render_end = render_start + GRID_WIDTH * 8;
		while (x < render_end) {
			Y = (render_end - x) / (render_end - render_start) / 2;
			if (Y > 0.375) Y = 0.375;
			color_v[i] = (double)v_polarity * Y;
			sample[i++] = Y;
			x += step;
		}
uncolored:
		/* uncolored +V field */
		render_end = render_start + GRID_WIDTH * 10;
		while (x < render_end) {
			color_v[i] = 0.375;
			sample[i++] = 0.375;
			x += step;
		}
		/* uncolored +-U field */
		render_end = render_start + GRID_WIDTH * 12;
		while (x < render_end) {
			color_u[i] = (double)v_polarity * 0.375;
			sample[i++] = 0.375;
			x += step;
		}
		render_start = render_end;
		break;
	case 9:
		/* sawtooth +U and uncolored field */
		render_end = render_start + GRID_WIDTH * 8;
		while (x < render_end) {
			Y = (render_end - x) / (render_end - render_start) / 2;
			if (Y > 0.375) Y = 0.375;
			color_u[i] = Y;
			sample[i++] = Y;
			x += step;
		}
		goto uncolored;
	}

	/* draw vertical line */
	if (position >= 3 && position <= 6) {
		render_end = vline_start + GRID_WIDTH * 6 - RAMP_WIDTH;
		while (vline_x < render_end) {
			vline_i++;
			vline_x += step;
		}
		render_end += RAMP_WIDTH;
		while (vline_x < render_end) {
			sample[vline_i] = ramp((render_end - vline_x) / RAMP_WIDTH) * (sample[vline_i] - GRID_LEVEL) + GRID_LEVEL;
			vline_i++;
			vline_x += step;
		}
		render_end += RAMP_WIDTH;
		while (vline_x < render_end) {
			sample[vline_i] = ramp((render_end - vline_x) / RAMP_WIDTH) * (GRID_LEVEL - sample[vline_i]) + sample[vline_i];
			vline_i++;
			vline_x += step;
		}
	}

	*_x = x;
	*_i = i;
	return render_start;
}

static void draw_line(sample_t *sample, sample_t *color_u, sample_t *color_v, int i, double step, double x, double line_end, double x_start, double x_stop)
{
	double render_start, render_end;

	/* go to ramp start */
	render_start = x_start - RAMP_WIDTH;
	while (x < render_start && x < line_end) {
		i++;
		x += step;
	}
	/* ramp up to line */
	render_end = render_start + RAMP_WIDTH;
	while (x < render_end && x < line_end) {
		color_u[i] = color_v[i] = 0;
		sample[i] = ramp((x - render_start) / RAMP_WIDTH) * (CIRCLE_LEVEL - sample[i]) + sample[i];
		i++;
		x += step;
	}
	/* draw line */
	render_start = x_stop;
	while (x < render_start && x < line_end) {
		color_u[i] = color_v[i] = 0;
		sample[i++] = CIRCLE_LEVEL;
		x += step;
	}
	/* ramp down from line */
	render_end = render_start + RAMP_WIDTH;
	while (x < render_end && x < line_end) {
		color_u[i] = color_v[i] = 0;
		sample[i] = ramp((x - render_start) / RAMP_WIDTH) * (sample[i] - CIRCLE_LEVEL) + CIRCLE_LEVEL;
		i++;
		x += step;
	}
}

static void kreislinie(sample_t *sample, sample_t *color_u, sample_t *color_v, int i, double step, double x, double line_end, int line, double circle_radius)
{
	double dist_center = abs(CIRCLE_CENTER - line), y, oc, ic;
	double center_x = (line_end - x) / 2.0 + x;

	/* no radius, no circle */
	if (circle_radius < 0.1)
		return;

	/* check if we are  above or below outer circle */
	y = dist_center / ((double)GRID_HEIGHT * circle_radius);
	if (y > 1.0)
		return;

	/* calc outer circle */
	oc = sqrt(2*(y+1) - (y+1)*(y+1)) * GRID_WIDTH * circle_radius + (double)(line & 1)/40000000.0;

	/* check if we are above or below inner circle */
	y = dist_center / ((double)GRID_HEIGHT * circle_radius - CIRCLE_HEIGTH);
	if (y > 1.0) {
		/* draw outer circle only */
		draw_line(sample, color_u, color_v, i, step, x, line_end, center_x - oc, center_x + oc);
		return;
	}

	/* calc inner circle */
	ic = sqrt(2*(y+1) - (y+1)*(y+1)) * GRID_WIDTH * circle_radius;

	/* draw both circles */
	draw_line(sample, color_u, color_v, i, step, x, line_end, center_x - oc, center_x - ic);
	draw_line(sample, color_u, color_v, i, step, x, line_end, center_x + ic, center_x + oc);
}

/* render test image line starting with x and end with LINE_LENGTH
 *
 * sample: pointer to samples, starting at x = 0
 * samplerate: is the sample rate in hertz
 * x: at what x to start rendering
 * line: line no. to render
 */
int fubk_gen_line(sample_t *sample, double x, double samplerate, sample_t *color_u, sample_t *color_v, int v_polarity, double line_start, double line_end, int line, double radius, int color_bar, int grid_only, const char *station_id)
{
	double initial_x;
	double step = 1.0 / samplerate;
	int i = 0, initial_i;
	double render_start, render_end, center_x;
	int grid_count = 0;
	int in_mittelfeld;

	/* skip x to line_start */
	while (x < line_start && x < line_end) {
		i++;
		x += step;
	}
	if (x >= line_end)
		return i;
	initial_i = i;
	initial_x = x;

	/* calculate phase for ramp start of center line */
	center_x = (line_end - line_start) / 2.0 + line_start;

	/* check if we are are rendering middle field and set line number */
	if (grid_only)
		in_mittelfeld = -1;
	else if (color_bar)
		in_mittelfeld = 0;
	else {
		in_mittelfeld = line - (CENTER_LINE - GRID_HEIGHT*5) -GRID_LINES/2;
		if (in_mittelfeld >= GRID_HEIGHT*10)
			in_mittelfeld = -1;
	}

	/* calculate position in grid:
	 * get the distance below the center line (line - CENTER_LINE)
	 * then be sure not to get negative value: add a multiple of the grid period
	 * then use modulo to get the distance below the grid line */
	if (((line - CENTER_LINE + GRID_HEIGHT*20) % GRID_HEIGHT) < GRID_LINES) {
		if (in_mittelfeld < 0)
			goto no_mittelfeld;
		/* special case where the center line is always drawn */
		if (in_mittelfeld >= 198 && in_mittelfeld <= 201)
			goto no_mittelfeld;
		/* grid line before middle field */
		render_end = center_x - GRID_WIDTH*6.0;
		while (x < render_end && x < line_end) {
			sample[i++] = GRID_LEVEL;
			x += step;
		}
		if (x >= line_end)
			return i;
		render_start = render_end;
		/* middle field */
		render_start = mittelfeld(sample, samplerate, &i, &x, render_start, step, color_u, color_v, v_polarity, in_mittelfeld, station_id);
no_mittelfeld:
		/* grid line after middle field */
		while (x < line_end) {
			sample[i++] = GRID_LEVEL;
			x += step;
		}
	} else {
		while (1) {
			/* calculate position for next ramp:
			 * get the distance to center (center_x - x - RAMP_WIDTH)
			 * then be sure not to get negative value: add a multiple of the grid period
			 * then use fmod to get the next ramp start */
			render_start = fmod(center_x - x - RAMP_WIDTH + GRID_WIDTH*20.0, GRID_WIDTH) + x;
			/* draw background field up to grid start */
			while (x < render_start && x < line_end) {
				sample[i++] = FIELD_LEVEL;
				x += step;
			}
			if (x >= line_end)
				break;
			/* ramp up to grid level */
			render_end = render_start + RAMP_WIDTH;
			while (x < render_end && x < line_end) {
				sample[i++] = ramp((x - render_start) / RAMP_WIDTH) * (GRID_LEVEL - FIELD_LEVEL) + FIELD_LEVEL;
				x += step;
			}
			if (x >= line_end)
				break;
			render_start = render_end;
			/* middle field */
			if (in_mittelfeld >= 0) {
				if (++grid_count == 4)
					render_start = mittelfeld(sample, samplerate, &i, &x, render_start, step, color_u, color_v, v_polarity, in_mittelfeld, station_id);
			}
			/* ramp down to field level */
			render_end = render_start + RAMP_WIDTH;
			while (x < render_end && x < line_end) {
				sample[i++] = ramp((x - render_start) / RAMP_WIDTH) * (FIELD_LEVEL - GRID_LEVEL) + GRID_LEVEL;
				x += step;
			}
			if (x >= line_end)
				break;
		}
	}

	kreislinie(sample, color_u, color_v, initial_i, step, initial_x, line_end, line, radius);

	return i;
}
