/**
 * a manager allows to manipulate multiple canvas
 *
 * it's different from matplotlib, where the manager can only store one
 * canvas
 *
 * @property Element parent_element DOM element containing every canvas
 *                                  of this manager
 * @property 1darray<Canvas> list_canvas List of canvas held by this
 *                                       manager
 */
class Manager
{
	/**
	 * @param Element parent_element Element holding the canvas
	 */
	constructor(parent_element)
	{
		this.parent_element = parent_element;
		this.list_canvas    = [];
	}

	/**
	 * add a canvas
	 *
	 * @param int width    Width of the added canvas
	 * @param int height   Height of the added canvas
	 * @param int position Subjective position of the added canvas
	 */
	add_canvas(width = 100, height = 100, position = 0)
	{
		let element_canvas = document.createElement('canvas');
		let text           = document.createTextNode(
			'Your browser doesn\'t seem to support canvas, or you ' +
			'deactivated them. This is the canvas where the chart ' +
			'should be displayed'
		);
		element_canvas.appendChild(text);
		if (this.parent_element.childElementCount === 0)
		{
			this.parent_element.appendChild(element_canvas);
		}
		else
		{
			this.parent_element.children[position].after(
				element_canvas
			);
		}
		let ctx = element_canvas.getContext('2d');
		ctx.canvas.width  = width ;
		ctx.canvas.height = height;
		let figure = new Figure();
		let canvas = new Canvas(ctx, figure);
		this.list_canvas.splice(position, 0, canvas);
		return canvas;
	}

	/**
	 * draw every canvas
	 */
	draw()
	{
		for (const index in this.list_canvas)
		{
			this.list_canvas[index].draw();
		}
	}

	/**
	 * remove a canvas
	 *
	 * @param int position Position of the canvas to remove
	 */
	remove_canvas(position = 0)
	{
		document.removeChild(this.list_canvas[position].ctx.canvas);
		this.list_canvas.splice(position, 1);
	}
}

/**
 * a canvas allows to render a figure
 *
 * @property CanvasRenderingContext2D ctx Canvas API interface
 * @property Figure figure Figure held by this canvas
 */
class Canvas
{
	/**
	 * @param CanvasRenderingContext2D ctx Canvas API interface
	 * @param Figure figure Figure held by this canvas
	 */
	constructor(ctx, figure)
	{
		this.ctx    = ctx   ;
		this.figure = figure;
	}

	/**
	 * render the figure to the canvas
	 */
	draw()
	{
		if (this.figure instanceof Figure.constructor)
		{
			throw _('this canvas does not possess figure to draw')
		}
		let start_x = 0;
		for (const index_axes in this.figure.list_axes)
		{
			let axes = this.figure.list_axes[index_axes];
			let x_proportion = axes.width/this.figure.width;
			let end_x = start_x + this.ctx.canvas.width * x_proportion;
			axes.draw(
				this.ctx,
				[
					[start_x, 0                     ],
					[end_x  , this.ctx.canvas.height],
				],
			);
			start_x = end_x;
		}
	}
}

/**
 * a figure hold axes and every plot elements
 *
 * @property int width Total width of this figure (related to each
 *                     axes)
 * @property int height Total height of this figure (related to each
 *                     axes)
 * @property 1darray<Axes> list_axes List of every axes
 */
class Figure
{
	constructor()
	{
		this.width     = 0 ;
		this.height    = 0 ;
		this.list_axes = [];
	}

	/**
	 * add axes to this figure
	 *
	 * @param int relative_width Relative width of the new axes
	 * @param int relative_height Relative height of the new axes
	 * @param int position Position of the axes in the figure
	 */
	add_axes(relative_width = 50, relative_height = 50, position = 0)
	{
		this.width  += relative_width ;
		this.height += relative_height;
		let axes = new Axes(relative_width, relative_height);
		this.list_axes.splice(position, 0, axes);
		return axes;
	}

	/**
	 * remove an axes
	 *
	 * @param int position Position of the axes in the figure to delete
	 */
	remove_axes(position = 0)
	{
		this.list_axes.splice(position, 1);
	}
}

/**
 * an axes represents one plot in a figure.
 *
 * @property int width Width of the axes
 * @property int height Height of the axes
 * @property String title Title of the axes
 * @property 1darray<Line> lines List of lines
 */
class Axes
{
	/**
	 * @param int width Width of the axes
	 * @param int height Height of the axes
	 * @param String title Title of the axes
	 */
	constructor(
		width    = 100      ,
		height   = 100      ,
	)
	{
		this.width    = width;
		this.height   = height;
		this.axis     = new Axis();
		this.lines    = [];
	}

	/**
	 * plot y versus x as line
	 *
	 * @param 1darray<Number> x x data
	 * @param 1darray<Number> y y data
	 * @param String linestyle style of the line (can be "solid",
	 *                         "dotted", "dashdot", "dashed" or ""
	 *                         for no line)
	 * @param String linecolor color of the line (can be "#abcdef",
	 *                         "rgb(x,y,z)" or a name of a color.
	 * @param String markerstyle style of the marker used for each
	 *                           point (can be "circle", "point",
	 *                           "small", "cross" or "" for no
	 *                           marker)
	 * @param String markercolor color of the marker (same
	 *                           definition than linecolor)
	 * @param Number markersize size of the marker
	 */
	plot(
		x                     ,
		y                     ,
		linestyle   = 'solid' ,
		linecolor   = 'black' ,
		markerstyle = ''      ,
		markercolor = 'black' ,
		markersize  = 5       ,
	)
	{
		let line = new Line(
			x          ,
			y          ,
			linestyle  ,
			linecolor  ,
			markerstyle,
			markercolor,
			markersize ,
		);
		this.lines.push(line);
	}

	/**
	 * render the axes to the canvas
	 *
	 * @param ctx CanvasRenderingContext2D Context of the canvas
	 * @param view 2darray<Number> Window where to draw the canvas
	 */
	draw(ctx, view)
	{
		const x_min = get_ext_array(
			this.lines.map(
				(element) => element.x.reduce(
					(a, b) => Math.min(a, b),
					Infinity
				)
			)       ,
			Math.min,
			1
		);
		const y_min = get_ext_array(
			this.lines.map(
				(element) => element.y.reduce(
					(a, b) => Math.min(a, b),
					Infinity
				)
			)       ,
			Math.min,
			1
		);
		const x_max = get_ext_array(
			this.lines.map(
				(element) => element.x.reduce(
					(a, b) => Math.max(a, b),
					-Infinity
				)
			)
		);
		const y_max = get_ext_array(
			this.lines.map(
				(element) => element.y.reduce(
					(a, b) => Math.max(a, b),
					-Infinity
				)
			)
		);
		let n_view = view;
		if (this.title !== undefined)
		{
			n_view = this.title.draw(
				ctx   ,
				view,
			);
		}
		n_view = this.axis.draw(
			ctx   ,
			n_view,
			[ [x_min, y_min], [x_max, y_max] ],
		);
		for (const index_line in this.lines)
		{
			this.lines[index_line].draw(
				ctx        ,
				n_view,
				[
					[x_min, y_min],
					[x_max, y_max],
				]         ,
			);
		}
		ctx.beginPath();
		ctx.strokeStyle = 'black';
		ctx.setLineDash([]);
		ctx.strokeRect(
			n_view[0][0], n_view[0][1],
			n_view[1][0] - n_view[0][0],
			n_view[1][1] - n_view[0][1],
		);
	}

	/**
	 * title setter
	 *
	 * @param content String Content of the title
	 */
	set title(content)
	{
		this._title = new Title(content);
	}

	/**
	 * title getter
	 */
	get title()
	{
		return this._title;
	}
}

/**
 * a line, with a color and a line style
 *
 * @property 1darray<float> x xdata
 * @property 1darray<float> y ydata
 * @property String linestyle style of the line (can be "solid",
 *                         "dotted", "dashdot", "dashed" or ""
 *                         for no line)
 * @property String linecolor color of the line (can be "#abcdef",
 *                         "rgb(x,y,z)" or a name of a color.
 * @property String markerstyle style of the marker used for each
 *                           point (can be "circle", "point",
 *                           "small", "cross" or "" for no
 *                           marker)
 * @property String markercolor color of the marker (same
 *                           definition than linecolor)
 * @property Number markersize size of the marker
 */
class Line
{
	/**
	 * @param 1darray<float> x xdata
	 * @param 1darray<float> y ydata
	 * @param string linestyle style of the line (can be "solid",
	 *                         "dotted", "dashdot", "dashed" or ""
	 *                         for no line)
	 * @param string linecolor color of the line (can be "#abcdef",
	 *                         "rgb(x,y,z)" or a name of a color.
	 * @param string markerstyle style of the marker used for each
	 *                           point (can be "circle", "point",
	 *                           "small", "cross" or "" for no
	 *                           marker)
	 * @param string markercolor color of the marker (same
	 *                           definition than linecolor)
	 * @param Number markersize size of the marker
	 */
	constructor(
		x                      ,
		y                      ,
		linestyle   = 'solid'  ,
		linecolor   = 'black'  ,
		markerstyle = ''       ,
		markercolor = 'black'  ,
		markersize  = 5        ,
	)
	{
		this.x           = x          ;
		this.y           = y          ;
		this.linestyle   = linestyle  ;
		this.linecolor   = linecolor  ;
		this.markerstyle = markerstyle;
		this.markercolor = markercolor;
		this.markersize  = markersize ;
	}

	/**
	 * draw the line in the box coordinate
	 *
	 * @param CanvasRenderingContext2d ctx context of the canvas
	 * @param 2dlist<int>   box  box where to draw the line
	 * @param 2dlist<float> view scale of the box (link value <->
	 *                           coordinate
	 */
	draw(ctx, box, view)
	{
		const x_delta    = view[1][0] - view[0][0];
		const y_delta    = view[1][1] - view[0][1];
		const box_width  =  box[1][0] -  box[0][0];
		const box_height =  box[1][1] -  box[0][1];

		let x_coordinates = this.x.map(
			function (element)
			{
				return box[0][0] + box_width * (
					element - view[0][0]
				) / x_delta;
			}
		);
		let y_coordinates = this.y.map(
			function (element)
			{
				return box[0][1] + box_height - ( // starting top left
					box_height * (
						element - view[0][1]
					) / y_delta
				);
			}
		);
		if (this.linestyle != '')
		{
			this.drawLine(
				ctx,
				x_coordinates,
				y_coordinates,
			);
		}
		if (this.markerstyle != '')
		{
			this.drawPoints(
				ctx          ,
				x_coordinates,
				y_coordinates,
			);
		}
	}

	/**
	 * draw a line in the given context at the given positions
	 *
	 * @param CanvasRenderingContext2d ctx context of the canvas
	 * @param 1darray<Number> x_coordinates X coordinates of the point
	 * @param 1darray<Number> y_coordinates Y coordinates of the point
	 */
	drawLine(ctx, x_coordinates, y_coordinates)
	{
		ctx.beginPath();
		ctx.strokeStyle = this.linecolor;
		ctx.moveTo(
			x_coordinates[0],
			y_coordinates[0],
		);
		if (this.linestyle === 'solid')
		{
			ctx.setLineDash([]);
		}
		else if (this.linestyle === 'dashed')
		{
			ctx.setLineDash([15, 5]);
		}
		else if (this.linestyle === 'dotted')
		{
			ctx.setLineDash([2,2]);
		}
		else if (this.linestyle === 'dashdot')
		{
			ctx.setLineDash([15,2,2,2]);
		}
		else
		{
			throw _('the style of the line is not correctly defined');
		}
		for (const index in x_coordinates)
		{
			ctx.lineTo(
				x_coordinates[index],
				y_coordinates[index],
			);
		}
		ctx.stroke();
	}

	/**
	 * draw points in the given context at the given positions
	 *
	 * @param CanvasRenderingContext2d ctx context of the canvas
	 * @param 1darray<Number> x_coordinates x coordinate of the point
	 * @param 1darray<Number> y_coordinates y coordinate of the point
	 */
	drawPoints(ctx, x_coordinates, y_coordinates)
	{
		for (const index in x_coordinates)
		{
			let x = x_coordinates[index];
			let y = y_coordinates[index];
			ctx.beginPath();
			if (this.markerstyle == 'circle')
			{
				ctx.fillStyle = this.markercolor;
				ctx.ellipse(
					x              ,
					y              ,
					this.markersize,
					this.markersize,
					0              ,
					0              ,
					2*Math.PI      ,
				);
				ctx.fill();
			}
			else if (this.markerstyle == 'point')
			{
				ctx.fillStyle = this.markercolor;
				ctx.ellipse(
					x                  ,
					y                  ,
					0.5*this.markersize,
					0.5*this.markersize,
					0              ,
					0              ,
					2*Math.PI      ,
				);
				ctx.fill();
			}
			else if (this.markerstyle == 'small')
			{
				ctx.fillStyle = this.markercolor;
				ctx.ellipse(
					x                  ,
					y                  ,
					0.1*this.markersize,
					0.1*this.markersize,
					0              ,
					0              ,
					2*Math.PI      ,
				);
				ctx.fill();
			}
			else if (this.markerstyle == 'cross')
			{
				ctx.setLineDash([]);
				ctx.strokeStyle = this.markercolor;
				ctx.moveTo(
					x - this.markersize,
					y - this.markersize,
				);
				ctx.lineTo(
					x + this.markersize,
					y + this.markersize,
				);
				ctx.stroke();
				ctx.beginPath();
				ctx.moveTo(
					x + this.markersize,
					y - this.markersize,
				);
				ctx.lineTo(
					x - this.markersize,
					y + this.markersize,
				);
				ctx.stroke();
			}
			else
			{
				throw _('the style of the marker is not correctly defined');
			}
		}
	}
}

/**
 * font object
 *
 * @property size int font-size (px)
 * @property family string font-family
 * @property color string font color
 */
class Font
{
	/**
	 * @param size int font-size (px)
	 * @param family string font-family
	 * @param color string font color
	 */
	constructor(
		size   = 20          ,
		family = 'sans-serif',
		color  = 'black'     ,
	)
	{
		this.size   = size;
		this.family = family;
		this.color  = color;
	}

	/**
	 * Convert font to css font proprety's syntax
	 *
	 * @return String
	 */
	toString()
	{
		return this.size.toString() + 'px ' + self.family;
	}
}

/**
 * title of an axes
 *
 * @property content String Content of a title
 * @property font Font Font of a title
 */
class Title
{
	/**
	 * @param content Content Content of a title
	 * @param font Font Font of a title
	 */
	constructor(
		content = ''       ,
		font    = undefined,
		margin  = 5        ,
	)
	{
		this.content = content;
		if (font === undefined)
		{
			font = new Font();
		}
		this.font = font;
		this.margin = margin;
	}

	/**
	 * draw the title in the given windows
	 *
	 * @param ctx CanvasRenderingContext2D Context of the canvas
	 * @param box int<array> Window where to write the title on
	 *
	 * @return plot_box int<array> New windows for the plot
	 */
	draw(ctx, box)
	{
		ctx.font = this.font.toString();
		ctx.fillStyle = 'black';
		ctx.strokeStyle = 'black';
		let width = ctx.measureText(this.content).width;
		if (width > box[1][0] - box[0][0] + 2 * this.margin)
		{
			ctx.fillText(
				this.content      ,
				box[0][0]      ,
				this.font.size + this.margin,
				box[1][0] - box[0][0],
			);
		}
		else
		{
			let text_position = box[0][0] + (
				box[1][0] - box[0][0] - width
			) / 2
			ctx.fillText(
				this.content,
				text_position,
				this.font.size + this.margin,
			);
		}
		return [
			[
				box[0][0],
				box[0][1] + this.font.size + 2 * this.margin,
			],
			[box[1][0], box[1][1]],
		];
	}
}

/**
 * Axis (x and y) of the axes (not separated xaxis and yaxis)
 *
 * @property int number_x_tick Number of x axis tick
 * @property int number_y_tick Number of y axis tick
 * @property int size_tick Size of the tick
 * @property int margin Size of the margin
 * @property Font font Font of tick labels
 */
class Axis
{
	/**
	 * @param number_x_tick int Number of x axis tick
	 * @param number_y_tick int Number of y axis tick
	 * @param size_tick int Size of the tick
	 * @param margin int Size of the margin
	 * @param text_height int Height of the text label
	 * @param Font font Font of tick labels
	 */
	constructor(
		number_x_tick = 4        ,
		number_y_tick = 4        ,
		size_tick     = 5        ,
		margin        = 15       ,
		text_height   = 15       ,
		font          = undefined,
	)
	{
		this.number_x_tick = number_x_tick;
		this.number_y_tick = number_y_tick;
		this.size_tick = size_tick;
		this.margin = margin;
		this.text_height = text_height;
		if (font === undefined)
		{
			font = new Font();
		}
		this.font = font;
	}

	/**
	 * draw xaxis and yaxis to canvas
	 *
	 * @param ctx CanvasRenderingContext2D Context of canvas
	 * @param box 2darray<Number> Window where to draw the axis
	 * @param view 2darray<Number> Window where to draw the canvas with the
	 *                             real value
	 * @return 2darray<Number> New window where to draw the rest of the plot
	 */
	draw(ctx, box, view)
	{
		ctx.font = this.font.toString();
		ctx.strokeStyle = 'black';
		ctx.fillStyle   = 'black';
		let index = 0;
		let start_pos = box[0][1];
		let padding = (
			box[1][1] - box[0][1] - this.margin - this.text_height - this.size_tick
		) / this.number_y_tick;
		let step = (view[1][1] - view[0][1]) / this.number_y_tick;
		let width = 2 * this.text_height;
		while (index <= this.number_y_tick)
		{
			let current_y = start_pos + index * padding;
			let text      = (
				view[1][1] - index * step
			).toString().slice(0,3);
			let text_position = current_y + this.text_height / 2
			ctx.beginPath();
			ctx.moveTo(
				box[0][0] + this.margin + width,
				current_y,
			);
			ctx.lineTo(
				box[0][0] + this.margin + this.size_tick + width,
				current_y,
			);
			ctx.stroke();
			ctx.fillText(
				text,
				box[0][0] + this.margin,
				text_position,
			);
			current_y += padding;
			index += 1;
		}
		index = 0;
		start_pos = box[0][0] + this.margin + this.size_tick
			+ width;
		padding = (
			box[1][0] - this.margin - start_pos
		) / this.number_x_tick;
		step = (view[1][0] - view[0][0]) / this.number_x_tick;
		while (index <= this.number_x_tick)
		{
			let current_x = start_pos + padding * index;
			let text      = (
				view[0][0] + index * step
			).toString().slice(0,3);
			let text_width = ctx.measureText(text).width;
			let text_position = current_x - text_width / 2;
			ctx.strokeStyle = 'black';
			ctx.beginPath();
			ctx.moveTo(
				current_x,
				box[1][1] - this.margin - this.text_height,
			);
			ctx.lineTo(
				current_x,
				box[1][1] - this.margin - this.size_tick - this.text_height,
			);
			ctx.stroke();
			ctx.fillText(
				text         ,
				text_position,
				box[1][1] - this.margin,
			);
			index += 1;
		}
		return [
			[box[0][0] + this.margin + this.size_tick + width, box[0][1]],
			[box[1][0] - this.margin , box[1][1] - this.text_height - this.margin - this.size_tick],
		];
	}
}

/**
 * state-based interface to lichartee. It provides an implicit,
 * MATLAB-like, way of plotting. Needs a `<main>` node in the html
 *
 * @property Manager manager Current managed manager
 */
class jsplot
{
	/**
	 * @param Manager manager Current managed manager
	 */
	constructor()
	{
		this.manager = new Manager(
			document.getElementsByTagName('main')[0]
		);
	}

	/**
	 * plot y versus x as lines and/or markers
	 *
	 * @param 1darray<Number> x x data
	 * @param 1darray<Number> y y data
	 * @param String linestyle style of the line (can be "solid",
	 *                         "dotted", "dashdot", "dashed" or ""
	 *                         for no line)
	 * @param String linecolor color of the line (can be "#abcdef",
	 *                         "rgb(x,y,z)" or a name of a color.
	 * @param String markerstyle style of the marker used for each
	 *                           point (can be "circle", "point",
	 *                           "small", "cross" or "" for no
	 *                           marker)
	 * @param String markercolor color of the marker (same
	 *                           definition than linecolor)
	 * @param Number markersize size of the marker
	 */
	plot(
		x          ,
		y          ,
		linestyle  ,
		linecolor  ,
		markerstyle,
		markercolor,
		markersize ,
	)
	{
		if (this.manager.list_canvas.length === 0)
		{
			this.manager.add_canvas(500,500);
		}
		let canvas = this.manager.list_canvas[
			this.manager.list_canvas.length - 1
		];
		canvas = this.manager.list_canvas[0];
		if (canvas.figure.list_axes.length === 0)
		{
			canvas.figure.add_axes();
		}
		canvas.figure.list_axes[0].plot(
			x          ,
			y          ,
			linestyle  ,
			linecolor  ,
			markerstyle,
			markercolor,
			markersize ,
		);
	}

	/**
	 * set a title for the Axes
	 *
	 * @param String content Text to use for the title
	 */
	title(content)
	{
		if (this.manager.list_canvas.length === 0)
		{
			this.manager.add_canvas(500,500);
		}
		let canvas = this.manager.list_canvas[0];
		if (canvas.figure.list_axes.length === 0)
		{
			canvas.figure.add_axes();
		}
		canvas.figure.list_axes[0].title = content;
	}

	/**
	 * render the current canvas and create a new canvas for next a plot
	 */
	show()
	{
		if (this.manager.list_canvas.length === 0)
		{
			this.manager.add_canvas(
				500                        ,
				500                        ,
				this.manager.list_canvas[0],
			);
		}
		else
		{
			this.manager.list_canvas[0].draw();
			this.manager.add_canvas(
				500                                ,
				500                                ,
				this.manager.list_canvas.length - 1,
			);
		}
	}
}

/**
 * get max or min of a 1d array of number
 *
 * @param 1darray<number> array Array where to get max values
 * @param function f Math.max or Math.min if we want max or min value
 * @param int sign -1 if max, 1 if min
 */
function get_ext_array(array, f = Math.max, sign = -1)
{
	return array.reduce(
		(a, b) => f(a, b),
		sign * Infinity
	);
}