/*============================================================================
  BMPGraphing, a library for graphing.
  Copyright (C) 2005-2017 by Zack T Smith.

  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 2 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, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

  The author may be reached at 1@zsmith.co.
 *===========================================================================*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#include "BMP.h"
#include "BMPGraphing.h"

//----------------------------------------------------------------------------
// Name:	BMPGraphing_draw_labels_log2
// Purpose:	Draw the labels and ticks.
//----------------------------------------------------------------------------
void
BMPGraphing_draw_labels_log2 (BMPGraph* graph)
{
	if (!graph || !graph->image)
		return;

	//----------------------------------------
	// Horizontal
	//
	// Establish min & max x values.
	//
	int i = 0;
	Value min_x = 0x4000000000000000;
	Value max_x = 0;
	for (i = 0; i < graph->data_index; i += 2) {
		Value type = graph->data[i];
		Value value = graph->data[i+1];
		if (type == DATUM_X) {
			if (value < min_x)
				min_x = value;
			if (value > max_x)
				max_x = value;
		}
	}
	graph->min_x = (long long) log2 (min_x);
	graph->max_x = (long long) ceil (log2 (max_x));

	for (i = graph->min_x; i <= graph->max_x; i++) {
		char str [200];
		int x = graph->left_margin + 
			((i-graph->min_x) * graph->x_span) / 
			(graph->max_x - graph->min_x);
		int y = graph->height - graph->margin + 10;

		unsigned long y2 = 1 << i;
		if (y2 < 1536) 
			snprintf (str, 199, "%ld B", y2);
		else if (y2 < (1<<20)) {
			snprintf (str, 199, "%ld kB", y2 >> 10);
		}
		else {
			Value j = y2 >> 20;
			switch ((y2 >> 18) & 3) {
			case 0: snprintf (str, 199, "%lld MB", (unsigned long long) j); break;
			case 1: snprintf (str, 199, "%lld.25 MB", (unsigned long long) j); break;
			case 2: snprintf (str, 199, "%lld.5 MB", (unsigned long long) j); break;
			case 3: snprintf (str, 199, "%lld.75 MB", (unsigned long long) j); break;
			}
		}

		BMP_vline (graph->image, x, y, y - 10, RGB_BLACK);
		BMP_draw_mini_string (graph->image, str, x - 10, y + 8, RGB_BLACK);
	}

	//----------------------------------------
	// Vertical
	//
	// Establish min & max y values.
	//
	Value min_y = 0x4000000000000000;
	Value max_y = 0;
	for (i = 0; i < graph->data_index; i += 2) {
		Value type = graph->data[i];
		Value value = graph->data[i+1];
		if (type == DATUM_Y) {
			if (value < min_y)
				min_y = value;
			if (value > max_y)
				max_y = value;
		}
	}
	graph->min_y = min_y;
	graph->max_y = max_y;

	int font_height = 10;
	int available_height = graph->y_span;
	int max_labels = available_height / font_height;
	int preferred_n_labels = graph->max_y/10000;
	int actual_n_labels;
	float multiplier = 1;
	if (preferred_n_labels < max_labels) {
		actual_n_labels = preferred_n_labels;
	} else {
		actual_n_labels = max_labels;
		multiplier = preferred_n_labels / (float) actual_n_labels;
	}

	for (i = 0; i <= actual_n_labels; i++) {
		char str [200];
		int x = graph->left_margin - 10;
		int y = graph->height - graph->margin - (i * graph->y_span) / (float)actual_n_labels;

		BMP_hline (graph->image, x, x+10, y, RGB_BLACK);

		int value = (int) (i * multiplier);
		snprintf (str, 199, "%d GB/s", value);
		BMP_draw_mini_string (graph->image, str, x - 40, y - MINIFONT_HEIGHT/2, RGB_BLACK);
	}
}

BMPGraph *
BMPGraphing_new (int w, int h, int x_axis_mode)
{
	if (x_axis_mode != MODE_X_AXIS_LINEAR && x_axis_mode != MODE_X_AXIS_LOG2)
		return NULL;

	BMPGraph *graph = (BMPGraph*) malloc (sizeof(BMPGraph));
	if (!graph)
		return NULL;

	bzero (graph, sizeof(BMPGraph));

	graph->x_axis_mode = x_axis_mode;

	if (w <= 0 || h <= 0) {
		w = 1920;
		h = 1080;
	}

	graph->width = w;
	graph->height = h;
	graph->image = BMP_new (w, h);
	graph->margin = 40;
	graph->left_margin = 80;

	BMP_clear (graph->image, RGB_WHITE);

	BMP_hline (graph->image, graph->left_margin, graph->width - graph->margin, graph->height - graph->margin, RGB_BLACK);
	BMP_vline (graph->image, graph->left_margin, graph->margin, graph->height - graph->margin, RGB_BLACK);

	graph->x_span = graph->width - (graph->margin + graph->left_margin);
	graph->y_span = graph->height - 2 * graph->margin;

	graph->legend_y = graph->margin;

	return graph;
}

void BMPGraphing_set_title (BMPGraph* graph, const char *title)
{
	if (!graph || !title)
		return;

	if (graph->title)
		free (graph->title);
	graph->title = strdup (title);

	BMP_draw_string (graph->image, graph->title, graph->left_margin, graph->margin/2, RGB_BLACK);
}

void
BMPGraphing_new_line (BMPGraph *graph, const char *str, RGB color)
{
	if (!graph || !graph->image)
		return;

	BMP_draw_string (graph->image, str, graph->width - graph->margin - 370, graph->legend_y, 0xffffff & color);
	
	graph->legend_y += 17;

	graph->fg = 0;
	graph->last_x = graph->last_y = -1;

	if (graph->data_index >= MAX_GRAPH_DATA-2) 
		return; // error ("Too many graph data.");

	graph->data [graph->data_index++] = DATUM_COLOR;
	graph->data [graph->data_index++] = color;
}

//----------------------------------------------------------------------------
// Name:	BMPGraphing_add_point
// Purpose:	Adds a point to this list to be drawn.
//----------------------------------------------------------------------------
void
BMPGraphing_add_point (BMPGraph *graph, Value x, Value y)
{
	if (!graph || !graph->image)
		return;

	if (graph->data_index >= MAX_GRAPH_DATA-4) 
		return; // error ("Too many graph data.");

	graph->data [graph->data_index++] = DATUM_X;
	graph->data [graph->data_index++] = x;
	graph->data [graph->data_index++] = DATUM_Y;
	graph->data [graph->data_index++] = y;
}

//----------------------------------------------------------------------------
// Name:	BMPGraphing_plot_log2
// Purpose:	Plots a point on the current graph.
//----------------------------------------------------------------------------

void
BMPGraphing_plot_log2 (BMPGraph *graph, Value x, Value y)
{
	if (!graph || !graph->image)
		return;

	//----------------------------------------
	// Plot the point. The x axis is 
	// logarithmic, base 2.
	//
	double tmp = log2 (x);
	tmp -= (double) graph->min_x;
	tmp *= (double) graph->x_span;
	tmp /= (double) (graph->max_x - graph->min_x);

	int x2 = graph->left_margin + (int) tmp;
	int y2 = graph->height - graph->margin - (y * graph->y_span) / graph->max_y;

	if (graph->last_x != -1 && graph->last_y != -1) {
		if (graph->fg & DASHED) 
			BMP_line_dashed (graph->image, graph->last_x, graph->last_y, x2, y2, graph->fg & 0xffffff);
		else
			BMP_line (graph->image, graph->last_x, graph->last_y, x2, y2, graph->fg);
	}

	graph->last_x = x2;
	graph->last_y = y2;
}

//----------------------------------------------------------------------------
// Name:	BMPGraphing_plot_linear
// Purpose:	Plots a point on the current graph.
//----------------------------------------------------------------------------

void
BMPGraphing_plot_linear (BMPGraph *graph, Value x, Value y, Value max_y)
{
	if (!graph || !graph->image)
		return;

	//----------------------------------------
	// Plot the point. The x axis is 
	// logarithmic, base 2. The units of the
	// y value is kB.
	//
	double tmp = 10. + log2 (x);
	tmp -= (double) XVALUE_MIN;
	tmp *= (double) graph->x_span;
	tmp /= (double) (XVALUE_MAX - XVALUE_MIN);
	int x2 = graph->left_margin + (int) tmp;
	int y2 = graph->height - graph->margin - (y * graph->y_span) / max_y;

//printf ("\tx=%d, y=%d\n",x,y); fflush(stdout);

	if (graph->last_x != -1 && graph->last_y != -1) {
		if (graph->fg & DASHED) 
			BMP_line_dashed (graph->image, graph->last_x, graph->last_y, x2, y2, graph->fg & 0xffffff);
		else
			BMP_line (graph->image, graph->last_x, graph->last_y, x2, y2, graph->fg);
	}

	graph->last_x = x2;
	graph->last_y = y2;
}

//----------------------------------------------------------------------------
// Name:	BMPGraphing_make_log2
// Purpose:	Plots all lines.
//----------------------------------------------------------------------------

static void
BMPGraphing_make_log2 (BMPGraph *graph)
{
	if (!graph || !graph->image)
		return;

	BMPGraphing_draw_labels_log2 (graph);

	//----------------------------------------
	// OK, now draw the lines.
	//
	int i;
	int x = -1, y = -1;
	for (i = 0; i < graph->data_index; i += 2) 
	{
		Value type = graph->data[i];
		Value value = graph->data[i+1];

		switch (type) {
		case DATUM_Y:	y = value; break;
		case DATUM_X:	x = value; break;
		case DATUM_COLOR:	
			graph->fg = (unsigned long) value; 
			graph->last_x = -1;
			graph->last_y = -1;
			break;
		}

		if (x != -1 && y != -1) {
			BMPGraphing_plot_log2 (graph, x, y);
			x = y = -1;
		}
	}
}

//----------------------------------------------------------------------------
// Name:	BMPGraphing_make_linear
// Purpose:	Plots all lines for the network test graph.
//----------------------------------------------------------------------------

static void
BMPGraphing_make_linear (BMPGraph *graph)
{
	if (!graph || !graph->image)
		return;

	int i;

	// No data
	if (!graph->data_index)
		return;

	//----------------------------------------
	// Get the maximum bandwidth in order to
	// properly scale the graph vertically.
	//
	int max_y = 0;
	for (i = 0; i < graph->data_index; i += 2) {
		if (graph->data[i] == DATUM_Y) {
			int y = graph->data [i+1];
			if (y > max_y)
				max_y = y;
		}
	}

	int range = max_y > 10000 ? 2 : (max_y > 1000 ? 1 : 0);
	int y_spacing = 1;
	switch (range) {
	case 2:
		// Round up to the next 100.00 MB/sec. (=10000).
		y_spacing = 10000;
		break;
	case 1:
		// Round up to the next 10.00 MB/sec. 
		y_spacing = 1000;
		break;
	case 0:
		// Round up to the next 1.00 MB/sec. 
		y_spacing = 100;
		break;
	} 
	max_y /= y_spacing;
	max_y *= y_spacing;
	max_y += y_spacing;

	//----------------------------------------
	// Draw the axes, ticks & labels.
	//
	// X axis:
	if (XVALUE_MIN < 10)
		return; // error ("Minimum y is too small.");

	for (i = XVALUE_MIN; i <= XVALUE_MAX; i++) {
		char str[200];
		unsigned long y2 = 1 << (i-10); // XX XVALUE_MIN>=10
		if (y2 < 1024)
			snprintf (str, 199, "%u kB", (unsigned int) y2);
		else
			snprintf (str, 199, "%lu MB", (unsigned long) (y2 >> 10));

		int x = graph->left_margin + ((i - XVALUE_MIN) * graph->x_span) / (XVALUE_MAX - XVALUE_MIN);
		int y = graph->height - graph->margin + 10;
		
		BMP_vline (graph->image, x, y, y-10, RGB_BLACK);
		BMP_draw_mini_string (graph->image, str, x - 10, y+8, RGB_BLACK);
	}

	//----------
	// Y axis:
	// Decide what the tick spacing will be.
	for (i = 0; i <= max_y; i += y_spacing) {
		char str[200];
		unsigned long whole = i / 100;
		unsigned long frac = i % 100;
		snprintf (str, 199, "%lu.%02lu MB/s", whole, frac);

		int x = graph->left_margin - 10;
		int y = graph->height - graph->margin - (i * graph->y_span) / max_y;

		BMP_hline (graph->image, x, x+10, y, RGB_BLACK);
		BMP_draw_mini_string (graph->image, str, x - 60, y - MINIFONT_HEIGHT/2, RGB_BLACK);
	}

	//----------------------------------------
	// Draw the data lines.
	//
	int x = -1, y = -1;
	graph->last_x = -1;
	graph->last_y = -1;
	for (i = 0; i < graph->data_index; i += 2) 
	{
		int type = graph->data[i];
		long value = graph->data[i+1];

		switch (type) {
		case DATUM_Y:	y = value; break;
		case DATUM_X:	x = value; break;
		case DATUM_COLOR:	
			graph->fg = (unsigned long) value; 
			graph->last_x = -1;
			graph->last_y = -1;
			break;
		}

		if (x != -1 && y != -1) {
			BMPGraphing_plot_linear (graph, x, y, max_y);
			x = y = -1;
		}
	}
}

void
BMPGraphing_make (BMPGraph *graph)
{
	if (!graph)
		return; // XX silent error

	switch (graph->x_axis_mode) {
	case MODE_X_AXIS_LOG2:
		BMPGraphing_make_log2 (graph);
		break;
	case MODE_X_AXIS_LINEAR:
		BMPGraphing_make_linear (graph);
		break;
	default:
		fprintf (stderr, "Invalid graph mode %d.\n", graph->x_axis_mode);
		break;
	}
}

void
BMPGraphing_destroy (BMPGraph *graph)
{
	if (!graph)
		return;

	if (graph->title) {
		free (graph->title);
		graph->title = NULL;
	}
	if (graph->image) {
		BMP_destroy (graph->image);
		graph->image = NULL;
	}

	free (graph);
}
