//+----------------------------------------------------------------------------+
//| Description:  Magic Set Editor - Program to make Magic (tm) cards          |
//| Copyright:    (C) 2001 - 2007 Twan van Laarhoven                           |
//| License:      GNU General Public License 2 or later (see file COPYING)     |
//+----------------------------------------------------------------------------+

// ----------------------------------------------------------------------------- : Includes

#include <gui/control/graph.hpp>
#include <util/alignment.hpp>
#include <gfx/gfx.hpp>
#include <wx/dcbuffer.h>

DECLARE_TYPEOF_COLLECTION(GraphAxisP);
DECLARE_TYPEOF_COLLECTION(GraphElementP);
DECLARE_TYPEOF_COLLECTION(GraphGroup);
DECLARE_TYPEOF_COLLECTION(GraphP);
DECLARE_TYPEOF_COLLECTION(int);
DECLARE_TYPEOF_COLLECTION(vector<int>);
DECLARE_TYPEOF_COLLECTION(String);
DECLARE_TYPEOF(map<String COMMA UInt>);

template <typename T> inline T sgn(T v) { return v < 0 ? -1 : 1; }

// ----------------------------------------------------------------------------- : Events

DEFINE_EVENT_TYPE(EVENT_GRAPH_SELECT);

// ----------------------------------------------------------------------------- : GraphData

GraphElement::GraphElement(const String& v1) {
	values.push_back(v1);
}
GraphElement::GraphElement(const String& v1, const String& v2) {
	values.push_back(v1);
	values.push_back(v2);
}


GraphData::GraphData(const GraphDataPre& d)
	: axes(d.axes)
{
	// total size
	size = (UInt)d.elements.size();
	// find groups on each axis
	size_t i = 0;
	FOR_EACH(a, axes) {
		map<String,UInt> counts; // note: default constructor for UInt() does initialize to 0
		FOR_EACH_CONST(e, d.elements) {
			counts[e->values[i]] += 1;
		}
		// TODO: allow some ordering in the groups
		if (a->numeric) {
			// TODO: start at something other than 0?
			// TODO: support fractions?
			size_t left = counts.size();
			int i = 0;
			while (left) {
				String is = String() << i++;
				map<String,UInt>::const_iterator it = counts.find(is);
				if (it == counts.end()) {
					// not found, add a 0 bar
					a->groups.push_back(GraphGroup(is, 0));
				} else {
					a->groups.push_back(GraphGroup(is, it->second));
					a->max = max(a->max, it->second);
					a->total += it->second;
					left--;
				}
				if (i > 100) {
					// prevent infinite loops if there are non-numeric entries
					// drop empty tail
					while (a->groups.size() > 1 && a->groups.back().size == 0) {
						a->groups.pop_back();
					}
					break;
				}
			}
		} else if (a->order) {
			// specific group order
			FOR_EACH_CONST(gn, *a->order) {
				UInt count = counts[gn];
				a->groups.push_back(GraphGroup(gn, count));
				a->max = max(a->max, count);
				a->total += count;
			}
		} else {
			FOR_EACH(c, counts) {
				a->groups.push_back(GraphGroup(c.first, c.second));
				a->max = max(a->max, c.second);
				a->total += c.second;
			}
		}
		// colors
		if (a->auto_color == AUTO_COLOR_NO && a->colors) {
			// use colors from the table
			FOR_EACH(g, a->groups) {
				map<String,Color>::const_iterator it = a->colors->find(g.name);
				if (it != a->colors->end()) {
					g.color = it->second;
				}
			}
		} else {
			// find some nice colors for the groups
			double hue = 0.6; // start hue
			bool first = true;
			FOR_EACH(g, a->groups) {
				double amount = a->auto_color == AUTO_COLOR_EVEN
				                  ? 1. / a->groups.size()
				                  : double(g.size) / a->total; // amount this group takes
				if (!first) hue += amount/2;
				g.color = hsl2rgb(hue, 1.0, 0.5);
				hue += amount / 2;
				first = false;
			}
		}
		++i;
	}
	// count elements in each position
	values.clear();
	FOR_EACH_CONST(e, d.elements) {
		// find index j in elements
		vector<int> group_nrs(axes.size(), -1);
		int i = 0;
		FOR_EACH(a, axes) {
			String v = e->values[i];
			int j = 0;
			FOR_EACH(g, a->groups) {
				if (v == g.name) {
					group_nrs[i] = j;
					break;
				}
				++j;
			}
			++i;
		}
		values.push_back(group_nrs);
	}
}

void GraphData::crossAxis(size_t axis1, size_t axis2, vector<UInt>& out) const {
	size_t a1_size = axes[axis1]->groups.size();
	size_t a2_size = axes[axis2]->groups.size();
	out.clear();
	out.resize(a1_size * a2_size, 0);
	FOR_EACH_CONST(v, values) {
		int v1 = v[axis1], v2 = v[axis2];
		if (v1 >= 0 && v2 >= 0) {
			out[a2_size * v1 + v2]++;
		}
	}
}


// ----------------------------------------------------------------------------- : Graph1D

void Graph1D::draw(RotatedDC& dc, const vector<int>& current, DrawLayer layer) const {
	draw(dc, axis < current.size() ? current.at(axis) : -1, layer);
}
bool Graph1D::findItem(const RealPoint& pos, const RealRect& rect, vector<int>& out) const {
	int i = findItem(pos, rect);
	if (i == -1) return false;
	else {
		out.clear();
		out.insert(out.begin(), data->axes.size(), -1);
		out.at(axis) = i;
		return true;
	}
}

// ----------------------------------------------------------------------------- : Graph2D

void Graph2D::setData(const GraphDataP& d) {
	Graph::setData(d);
	if (data->axes.size() <= max(axis1,axis2)) return;
	d->crossAxis(axis1,axis2,values);
}

// ----------------------------------------------------------------------------- : Bar Graph

/// Rectangle for the bar of a bar graph
RealRect bar_graph_bar(const RealRect& rect, int group, int group_count, int start, int end, int max) {
	double width_space = rect.width / group_count; // including spacing
	double width       = width_space / 5 * 4;
	double space       = width_space / 5;
	double step_height = rect.height / max; // multiplier for bar height
	int top    = rect.bottom() - start * step_height;
	int bottom = rect.bottom() - end   * step_height;
	if (bottom < top) swap(top,bottom);
	bottom += 1;
	return RealRect(
		rect.x + width_space * group + space / 2,
		top,
		width,
		bottom - top
	);
}
/// Which column of the bar graph with count bars is coordinate x in?
int find_bar_graph_column(double width, double x, int count) {
	double width_space = width / count; // including spacing
	double space       = width_space / 5;
	// Find column in which the point could be located
	int    col    = int(x / width_space);
	if (col < 0 || col >= count) return -1; // not a column
	double in_col = x - col * width_space;
	if (in_col < space / 2)               return -1; // left
	if (in_col > width_space - space / 2) return -1; // right
	return col;
}

void BarGraph::draw(RotatedDC& dc, int current, DrawLayer layer) const {
	if (!data) return;
	// Rectangle for bars
	RealRect rect = dc.getInternalRect();
	GraphAxis& axis = axis_data();
	int count = int(axis.groups.size());
	// Bar sizes
	if (layer == LAYER_SELECTION) {
		// Highlight current column
		Color bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
		if (current >= 0) {
			const GraphGroup& group = axis.groups[current];
			RealRect bar = bar_graph_bar(rect, current, count, 0, group.size, axis.max);
			dc.SetPen(*wxTRANSPARENT_PEN);
			dc.SetBrush(lerp(bg, group.color, 0.25));
			dc.DrawRectangle(bar.move(-5,-5,10,10));
			dc.SetBrush(lerp(bg, group.color, 0.5));
			dc.DrawRectangle(bar.move(-2,-2,4,4));
		}
	} else if (layer == LAYER_VALUES) {
		// Draw bars
		dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT));
		int i = 0;
		FOR_EACH_CONST(g, axis.groups) {
			// draw bar
			dc.SetBrush(g.color);
			dc.DrawRectangle(bar_graph_bar(rect, i++, count, 0, g.size, axis.max));
		}
	}
}
int BarGraph::findItem(const RealPoint& pos, const RealRect& rect) const {
	if (!data) return -1;
	if (pos.y > max(rect.top(), rect.bottom())) return -1; // below
	if (pos.y < min(rect.top(), rect.bottom())) return -1; // above
	return find_bar_graph_column(rect.width, pos.x - rect.x, (int)axis_data().groups.size());
}

// ----------------------------------------------------------------------------- : Bar Graph 2D

void BarGraph2D::draw(RotatedDC& dc, const vector<int>& current, DrawLayer layer) const {
	if (!data || data->axes.size() <= max(axis1,axis2)) return;
	// Rectangle for bars
	RealRect rect = dc.getInternalRect();
	GraphAxis& axis1 = axis1_data(); // the major axis
	GraphAxis& axis2 = axis2_data(); // the stacked axis
	int count = int(axis1.groups.size());
	// Bar sizes
	if (layer == LAYER_SELECTION) {
		// Highlight current column
		Color bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
		int cur1 = this->axis1 < current.size() ? current[this->axis1] : -1;
		int cur2 = this->axis2 < current.size() ? current[this->axis2] : -1;
		if (cur1 >= 0) {
			// draw that bar
			int start = 0;
			int j = 0;
			FOR_EACH_CONST(g2, axis2.groups) {
				int end = start + values[j + axis2.groups.size() * cur1];
				if (j == cur2 || cur2 < 0) {
					RealRect bar = bar_graph_bar(rect, cur1, count, start, end, axis1.max);
					dc.SetBrush(lerp(bg, g2.color, 0.25));
					dc.DrawRectangle(bar.move(-5,0,10,0));
					dc.SetBrush(lerp(bg, g2.color, 0.5));
					dc.DrawRectangle(bar.move(-2,0,4,0));
				}
				start = end;
				++j;
			}
		} else if (cur2 >= 0) {
			// entire row
		}
		// TODO
	} else if (layer == LAYER_VALUES) {
		// Draw bars
		dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT));
		for (int i = 0 ; i < count ; ++i) {
			// draw stacked bars
			int start = 0;
			int j = 0;
			FOR_EACH_CONST(g2, axis2.groups) {
				int end = start + values[j++ + axis2.groups.size() * i];
				dc.SetBrush(g2.color);
				dc.DrawRectangle(bar_graph_bar(rect, i, count, start, end, axis1.max));
				start = end;
			}
		}
	}
}
bool BarGraph2D::findItem(const RealPoint& pos, const RealRect& rect, vector<int>& out) const {
	if (!data || data->axes.size() <= max(axis1,axis2)) return false;
	if (pos.y > max(rect.top(), rect.bottom())) return false; // below
	if (pos.y < min(rect.top(), rect.bottom())) return false; // above
	// column
	GraphAxis& axis1 = axis1_data(); // the major axis
	int count = (int)axis1.groups.size();
	int col   = find_bar_graph_column(rect.width, pos.x - rect.x, count);
	if (col < 0) return false;
	// row
	int max_value = (int)axis1.max;
	int value = (rect.bottom() - pos.y) / rect.height * max_value;
	if (value < 0 || value > max_value) return false;
	// find row
	int row = -1;
	size_t vs = col * axis2_data().groups.size();
	for (int i = 0 ; i < count ; ++i) {
		value -= values[vs+i];
		if (value < 0) {
			// in this layer of the stack
			row = i;
			break;
		}
	}
	if (row == -1) return false;
	// done
	out.clear();
	out.insert(out.begin(), data->axes.size(), -1);
	out.at(this->axis1) = col;
	out.at(this->axis2) = row;
	return true;
}

// ----------------------------------------------------------------------------- : Pie Graph

void PieGraph::draw(RotatedDC& dc, int current, DrawLayer layer) const {
	if (!data) return;
	// Rectangle for the pie
	GraphAxis& axis = axis_data();
	RealRect rect = dc.getInternalRect();
	double size = min(rect.width, rect.height);
	RealSize pie_size(size, size);
	RealSize pie_size_large(size+20, size+20);
	RealPoint pie_pos = rect.position() + rect.size() / 2;
	//RealPoint pos = align_in_rect(ALIGN_MIDDLE_CENTER, RealSize(size,size), rect);
	// draw items
	if (layer == LAYER_VALUES) {
		Color fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
		dc.SetPen(fg);
		// draw pies
		double angle = 0;
		int i = 0;
		FOR_EACH_CONST(g, axis.groups) {
			// draw pie
			dc.SetBrush(g.color);
			if (g.size > 0) {
				double end_angle = angle + 2 * M_PI * (double)g.size / axis.total;
				dc.DrawEllipticArc(pie_pos, i == current ? pie_size_large : pie_size, angle, end_angle);
				angle = end_angle;
			}
			++i;
		}
		// draw spokes
		if (axis.groups.size() > 1) {
			angle = 0;
			FOR_EACH_CONST(g, axis.groups) {
				if (g.size > 0) {
					dc.DrawEllipticSpoke(pie_pos, pie_size, angle);
					angle += 2 * M_PI * (double)g.size / axis.total;
				}
			}
		}
	}
}
int PieGraph::findItem(const RealPoint& pos, const RealRect& rect) const {
	if (!data) return -1;
	// Rectangle for the pie
	GraphAxis& axis = axis_data();
	double size = min(rect.width, rect.height);
	RealPoint pie_pos = rect.position() + rect.size() / 2;
	// position in circle
	Vector2D delta = pos - pie_pos;
	if (delta.lengthSqr() > size*size) {
		return -1; // outside circle
	}
	double pos_angle = atan2(-delta.y, delta.x); // in range [-pi..pi]
	if (pos_angle < 0) pos_angle += 2 * M_PI;
	// find angle
	double angle = 0;
	int i = 0;
	FOR_EACH_CONST(g, axis.groups) {
		angle += 2 * M_PI * (double)g.size / axis.total;
		if (angle > pos_angle) return i;
		++i;
	}
	return -1; //should not happen
}

// ----------------------------------------------------------------------------- : Scatter Plot



// ----------------------------------------------------------------------------- : Graph Legend

// ----------------------------------------------------------------------------- : Graph label axis

void GraphLabelAxis::draw(RotatedDC& dc, int current, DrawLayer layer) const {
	RealRect rect = dc.getInternalRect();
	GraphAxis& axis = axis_data();
	int count = int(axis.groups.size());
	// Draw
	if (layer == LAYER_SELECTION) {
		// highlight selection
	} else if (layer != LAYER_AXES) {
		if (direction == HORIZONTAL) {
			double width = rect.width / count; // width of an item
			// Draw labels
			double x = rect.x;
			FOR_EACH_CONST(g, axis.groups) {
				// draw label, aligned bottom center
				RealSize text_size = dc.GetTextExtent(g.name);
				dc.SetClippingRegion(RealRect(x + 2, rect.bottom() + 3, width - 4, text_size.height));
				dc.DrawText(g.name, align_in_rect(ALIGN_TOP_CENTER, text_size, RealRect(x, rect.bottom() + 3, width, 0)));
				dc.DestroyClippingRegion();
				x += width;
			}
			// Draw lines
			Color bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
			Color fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
			if (draw_lines) {
				dc.SetPen(lerp(bg, fg, 0.5));
				for (int i = 0 ; i < count ; ++i) {
					dc.DrawLine(RealPoint(rect.x + i*width, rect.top()), RealPoint(rect.x + i*width, rect.bottom()));
				}
			}
			// always draw axis line
			dc.SetPen(fg);
			dc.DrawLine(rect.topLeft(), rect.bottomLeft());
		} else {
			// TODO
		}
	}
}
int GraphLabelAxis::findItem(const RealPoint& pos, const RealRect& rect) const {
	GraphAxis& axis = axis_data();
	int col;
	if (direction == HORIZONTAL) {
		col = (pos.x - rect.x) / rect.width  * axis.groups.size();
		if (pos.y < rect.bottom()) return -1;
	} else {
		col = (pos.y - rect.y) / rect.height * axis.groups.size();
		if (pos.x > rect.left()) return -1;
	}
	if (col < 0 || col >= (int)axis.groups.size()) return -1;
	return col;
}

// ----------------------------------------------------------------------------- : Graph value axis

void GraphValueAxis::draw(RotatedDC& dc, int current, DrawLayer layer) const {
	if (layer != LAYER_AXES) return;
	if (!data) return;
	// How many labels and lines to draw?
	RealRect rect = dc.getInternalRect();
	GraphAxis& axis = axis_data();
	double step_height = rect.height / axis.max; // height of a single value
	dc.SetFont(*wxNORMAL_FONT);
	UInt label_step = (UInt)floor(max(1.0, (dc.GetCharHeight() + 1) / step_height)); // values per labeled line
	// Colors
	Color bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
	Color fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
	// Draw backlines (horizontal) and value labels
	dc.SetPen(lerp(bg, fg, 0.5));
	for (UInt i = 0 ; i <= axis.max ; ++i) {
		if (i % label_step == 0) {
			int y = rect.bottom() - i * step_height;
			dc.DrawLine(RealPoint(rect.left() - 2, y), RealPoint(rect.right(), y));
			// draw label, aligned middle right
			String label; label << i;
			RealSize text_size = dc.GetTextExtent(label);
			dc.DrawText(label, align_in_rect(ALIGN_MIDDLE_RIGHT, text_size, RealRect(rect.x - 4, y, 0, 0)));
		}
	}
	// Draw axis
	dc.SetPen(fg);
	dc.DrawLine(rect.bottomLeft() - RealSize(2,0), rect.bottomRight());
}

// ----------------------------------------------------------------------------- : Graph with margins

void GraphWithMargins::draw(RotatedDC& dc, const vector<int>& current, DrawLayer layer) const {
	RealRect inner = dc.getInternalRect().move(margin_left, margin_top, - margin_left - margin_right, - margin_top - margin_bottom);
	if (upside_down) { inner.y += inner.height; inner.height = -inner.height; }
	Rotation new_size(0, inner);
	Rotater rot(dc, new_size);
	graph->draw(dc, current, layer);
}
bool GraphWithMargins::findItem(const RealPoint& pos, const RealRect& rect, vector<int>& out) const {
	RealRect inner = rect.move(margin_left, margin_top, - margin_left - margin_right, - margin_top - margin_bottom);
	if (upside_down) { inner.y += inner.height; inner.height = -inner.height; }
	return graph->findItem(pos, inner, out);
}
void GraphWithMargins::setData(const GraphDataP& d) {
	Graph::setData(d);
	graph->setData(d);
}

// ----------------------------------------------------------------------------- : Graph Container

void GraphContainer::draw(RotatedDC& dc, const vector<int>& current, DrawLayer layer) const {
	FOR_EACH_CONST(g, items) {
		g->draw(dc, current, layer);
	}
}
bool GraphContainer::findItem(const RealPoint& pos, const RealRect& rect, vector<int>& out) const {
	FOR_EACH_CONST_REVERSE(g, items) {
		if (g->findItem(pos, rect, out)) return true;
	}
	return false;
}
void GraphContainer::setData(const GraphDataP& d) {
	Graph::setData(d);
	FOR_EACH(g, items) {
		g->setData(d);
	}
}
void GraphContainer::add(const GraphP& graph) {
	items.push_back(graph);
}


// ----------------------------------------------------------------------------- : GraphControl

GraphControl::GraphControl(Window* parent, int id)
	: wxControl(parent, id)
{
	setLayout(GRAPH_TYPE_BAR);
}

void GraphControl::setLayout(GraphType type) {
	if (type == layout) return;
	GraphDataP data = graph ? graph->getData() : GraphDataP();
	switch (type) {
		case GRAPH_TYPE_BAR: {
			intrusive_ptr<GraphContainer> combined(new GraphContainer());
			combined->add(new_intrusive1<GraphValueAxis>(0));
			combined->add(new_intrusive2<GraphLabelAxis>(0, HORIZONTAL));
			combined->add(new_intrusive1<BarGraph>(0));
			graph = new_intrusive5<GraphWithMargins>(combined, 23,8,7,20);
			break;
		} case GRAPH_TYPE_PIE: {
			intrusive_ptr<GraphContainer> combined(new GraphContainer());
			combined->add(new_intrusive1<PieGraph>(0));
			graph = new_intrusive5<GraphWithMargins>(combined, 20,20,20,20);
			break;
		} case GRAPH_TYPE_STACK: {
			intrusive_ptr<GraphContainer> combined(new GraphContainer());
			combined->add(new_intrusive1<GraphValueAxis>(0));
			combined->add(new_intrusive2<GraphLabelAxis>(0, HORIZONTAL));
			combined->add(new_intrusive2<BarGraph2D>(0,1));
			graph = new_intrusive5<GraphWithMargins>(combined, 23,8,7,20);
			break;
		} case GRAPH_TYPE_SCATTER: {
			// TODO
		} default:
			graph = GraphP();
	}
	if (data && graph) graph->setData(data);
	layout = type;
}

void GraphControl::setData(const GraphDataPre& data) {
	setData(new_intrusive1<GraphData>(data));
}
void GraphControl::setData(const GraphDataP& data) {
	if (graph) {
		graph->setData(data);
		current_item.clear(); // TODO : preserve selection
		Refresh(false);
	}
}

void GraphControl::onPaint(wxPaintEvent&) {
	wxBufferedPaintDC dc(this);
	wxSize cs = GetClientSize();
	RotatedDC rdc(dc, 0, RealRect(RealPoint(0,0),cs), 1, QUALITY_LOW);
	rdc.SetPen(*wxTRANSPARENT_PEN);
	rdc.SetBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
	rdc.DrawRectangle(rdc.getInternalRect());
	if (graph) {
		for (int layer = LAYER_BOTTOM ; layer < LAYER_COUNT ; ++layer) {
			graph->draw(rdc, current_item, (DrawLayer)layer);
		}
	}
}

void GraphControl::onSize(wxSizeEvent&) {
	Refresh(false);
}

void GraphControl::onMouseDown(wxMouseEvent& ev) {
	if (!graph) return;
	wxSize cs = GetClientSize();
	if (graph->findItem(RealPoint(ev.GetX(), ev.GetY()), RealRect(RealPoint(0,0),cs), current_item)) {
		wxCommandEvent ev(EVENT_GRAPH_SELECT, GetId());
		ProcessEvent(ev);
		Refresh(false);
	}
}

bool GraphControl::hasSelection(size_t axis) const {
	return axis < current_item.size() && current_item[axis] >= 0;
}
String GraphControl::getSelection(size_t axis) const {
	if (!graph || axis >= current_item.size() || axis >= graph->getData()->axes.size()) return wxEmptyString;
	GraphAxis& a = *graph->getData()->axes[axis];
	int i = current_item[axis];
	if (i == -1 || (size_t)i >= a.groups.size()) return wxEmptyString;
	return a.groups[current_item[axis]].name;
}

BEGIN_EVENT_TABLE(GraphControl, wxControl)
	EVT_PAINT		(GraphControl::onPaint)
	EVT_SIZE		(GraphControl::onSize)
	EVT_LEFT_DOWN	(GraphControl::onMouseDown)
END_EVENT_TABLE  ()
