lib_plot_composite_objects
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © robbatt

//@version=5
import robbatt/lib_plot_objects/32 as D

// @description library building on top of lib_plot_objects for composite objects such as Triangles and Polygons
library("lib_plot_composite_objects", overlay = true)

///////////////////////////////////////////////////////
//#region COMPOSITE OBJECTS
///////////////////////////////////////////////////////

//@description Composite object of 3 chart.points and lines
//@field a first Corner
//@field b second Corner
//@field c third Corner
//@field args Wrapper for reusable arguments for line.new()
//@field plot_ab The line object to be added and plotted via draw()
//@field plot_ac The line object to be added and plotted via draw()
//@field plot_bc The line object to be added and plotted via draw()
export type Triangle 
	chart.point a
	chart.point b
	chart.point c
	D.LineArgs args = na
	D.Line ab = na
	D.Line ac = na
	D.Line bc = na


//@description Creates a new linefill object and displays it on the chart, filling the space between the Triangle lines plot_ab and plot_ac with the color specified in fill_color.
//@field triangle The Triangle object
//@field fill_color The color used to fill the space between the lines.
//@field plot The linefill object to be added and plotted via draw()
export type TriangleFill
	Triangle triangle
	D.LineFill plot = na


//@description Composite object of variable amount of chart.points and lines
//@field points array of points that make up the Polygon
//@field center Center point of the Polygon, can be used for a label and will be center for PolygonFill
//@field args Wrapper for reusable arguments for line.new()
//@field plot An array of Lines that form Polygon Border
export type Polygon
	chart.point[] points
	chart.point center = na
	D.LineArgs args = na
	D.Line[] plot = na


//@description Creates a new line segments toward the Polygon center and linefill objects, filling the space in between with the color specified in fill_color.
//@field poly the Polygon
//@field fill_color The color used to fill the space between the lines.
//@field plot_segments An array of helper lines to create linefills
//@field plot_fills An array of linefills that cover the Polygon surface
export type PolygonFill
	Polygon poly
	color fill_color = #dededede
	TriangleFill[] plot = na


///////////////////////////////////////////////////////
//#endregion COMPOSITE OBJECTS
///////////////////////////////////////////////////////

///////////////////////////////////////////////////////
//#region OBJECT INSPECTION / CONVERSION TO JSON read
///////////////////////////////////////////////////////

//@function converts object to json representation
//@param date_format optional date format for str.format'ing point bar_time coordinates (default: 'dd-MM-y HH:mm')
export method tostring(Triangle this, simple string date_format = 'dd-MM-y HH:mm') =>
	na(this)?'null':'{' + str.format('"a": {0}, "b": {1}, "c": "{2}", "args": {3}', this.a.tostring(date_format), this.b.tostring(date_format), this.c.tostring(date_format), this.args.tostring()) + '}'
	
//@function converts object to json representation
//@param date_format optional date format for str.format'ing point bar_time coordinates (default: 'dd-MM-y HH:mm')
export method tostring(TriangleFill this, simple string date_format = 'dd-MM-y HH:mm') =>
	na(this)?'null':'{' + str.format('"triangle": {0}', this.triangle.tostring(date_format)) + '}'
	
//@function converts object to json representation
//@param date_format optional date format for str.format'ing point bar_time coordinates (default: 'dd-MM-y HH:mm')
export method tostring(Polygon this, simple string date_format = 'dd-MM-y HH:mm') =>
	na(this)?'null':'{' + str.format('"points": {0}, "center": {1}, "args": {2}', this.points.tostring(date_format), this.center.tostring(date_format), this.args.tostring()) + '}'
	
//@function converts object to json representation
//@param date_format optional date format for str.format'ing point bar_time coordinates (default: 'dd-MM-y HH:mm')
export method tostring(PolygonFill this, simple string date_format = 'dd-MM-y HH:mm') =>
	na(this)?'null':'{' + str.format('"poly": {0}', this.poly.tostring(date_format)) + '}'


tostrings(this, simple string date_format = 'dd-MM-y HH:mm') =>
	strings = array.new<string>(this.size())
	for [i,item] in this
		strings.set(i, item.tostring(date_format))
	'[' + strings.join(', ') + ']'

//@function converts array of objecta to json representation
//@param date_format optional date format for str.format'ing point bar_time coordinates (default: 'dd-MM-y HH:mm')
export method tostring(Triangle[] this, simple string date_format = 'dd-MM-y HH:mm') => tostrings(this, date_format)
//@function converts array of objecta to json representation
//@param date_format optional date format for str.format'ing point bar_time coordinates (default: 'dd-MM-y HH:mm')
export method tostring(Polygon[] this, simple string date_format = 'dd-MM-y HH:mm') => tostrings(this, date_format)
//@function converts array of objecta to json representation
//@param date_format optional date format for str.format'ing point bar_time coordinates (default: 'dd-MM-y HH:mm')
export method tostring(TriangleFill[] this, simple string date_format = 'dd-MM-y HH:mm') => tostrings(this, date_format)
//@function converts array of objecta to json representation
//@param date_format optional date format for str.format'ing point bar_time coordinates (default: 'dd-MM-y HH:mm')
export method tostring(PolygonFill[] this, simple string date_format = 'dd-MM-y HH:mm') => tostrings(this, date_format)


///////////////////////////////////////////////////////
//#endregion OBJECT INSPECTION / CONVERSION TO JSON
///////////////////////////////////////////////////////


///////////////////////////////////////////////////////
//#region OBJECT CREATION / FACTORY METHODS create
///////////////////////////////////////////////////////

// Triangles

//@function Creates a new Triangle object.
export method create_triangle(chart.point this, chart.point b, chart.point c, D.LineArgs args = na) =>
	default_args = args.or_default()
	Triangle.new(this, b, c, default_args, D.Line.new(this, b, default_args), D.Line.new(this, c, default_args), D.Line.new(b, c, default_args))

//@function Creates a new Triangle object.
export method create_triangle(D.Line this, chart.point c) =>
	default_args = this.args.or_default()
	Triangle.new(this.start, this.end, c, default_args, this, D.Line.new(this.start, c, default_args), D.Line.new(this.end, c, default_args))

// Polygons

//@function Creates a new Polygon object.
export method create_polygon(chart.point[] points, D.LineArgs args = na) =>
	if na(points) ? true : points.size() < 2
		na
	else
		count = points.size()
		center = points.create_center()

		border = array.new<D.Line>(count)
		border_args = args.or_default()
		if count > 2
			first = points.get(0)
			last = points.get(count - 1)

			border.set(0, D.Line.new(last, first, border_args))
			for i = 1 to count - 1
				prev = points.get(i-1)
				curr = points.get(i)
				border.set(i, D.Line.new(prev, curr, border_args)) 

		Polygon.new(points, center, border_args, border)

//@function Creates a new Polygon object.
export method create_polygon(chart.point start, chart.point[] others, D.LineArgs args = na) =>array.from(start).concat(others).create_polygon(args)

//@function Creates a new TriangleFill object.
export method create_fill(Triangle this, color fill_color = #dededede) =>
	if not na(this)
		TriangleFill.new(this, D.create_fill(this.ab, this.ac, fill_color, support = this.bc))

//@function Creates a new PolygonFill object.
export method create_fill(Polygon this, color fill_color = #dededede) =>
	if not na(this)
		segments = array.new<TriangleFill>(0)
		if not na(this.plot)
			for border_line in this.plot
				segments.push(border_line.create_triangle(this.center).create_fill(fill_color = fill_color))
		PolygonFill.new(this, fill_color, segments)

//@function Creates a new D.Label object.
export method create_center_label(Triangle this, string txt = na, D.LabelArgs args = na, string tooltip = na) =>
	if not na(this)
		points = array.from(this.a, this.b, this.c)
		D.CenterLabel.new(points, D.Label.new(points.create_center(), txt, na(args) ? D.LabelArgs.new() : args, tooltip))

//@function Creates a new D.Label object.
export method create_label(Polygon this, string txt = na, D.LabelArgs args = na, string tooltip = na) =>
	if not na(this)
		D.CenterLabel.new(this.points, D.Label.new(this.points.create_center(), txt, na(args) ? D.LabelArgs.new() : args, tooltip))

_nz(this, default) =>
	na(this) ? default : this

export method nz(Triangle this,		Triangle default = na)	 		=>	not na(default) ? _nz(this, default) : _nz(this, Triangle.new(chart.point.now(), chart.point.now(), chart.point.now(), D.LineArgs.new()))
export method nz(TriangleFill this,	TriangleFill default = na)	 	=>	not na(default) ? _nz(this, default) : _nz(this, TriangleFill.new(Triangle.new(chart.point.now(), chart.point.now(), chart.point.now(), D.LineArgs.new())))
export method nz(Polygon this,		Polygon default = na)	 		=>	not na(default) ? _nz(this, default) : _nz(this, Polygon.new(array.from(chart.point.now(), chart.point.now(), chart.point.now()), chart.point.now(), D.LineArgs.new()))
export method nz(PolygonFill this,	PolygonFill default = na)	 	=>	not na(default) ? _nz(this, default) : _nz(this, PolygonFill.new(Polygon.new(array.from(chart.point.now(), chart.point.now(), chart.point.now()), chart.point.now(), D.LineArgs.new())))

///////////////////////////////////////////////////////
//#endregion OBJECT CREATION / FACTORY METHODS
///////////////////////////////////////////////////////

_enqueue(id, item, int max = 0) =>
	if not na(id)
    	id.unshift(item)
		if max > 0 and id.size() > max
			id.pop()
    id

export method enqueue(Triangle[] id, Triangle item, int max = 0) => _enqueue(id, item, max)
export method enqueue(Polygon[] id, Polygon item, int max = 0) => _enqueue(id, item, max)
export method enqueue(TriangleFill[] id, TriangleFill item, int max = 0) => _enqueue(id, item, max)
export method enqueue(PolygonFill[] id, PolygonFill item, int max = 0) => _enqueue(id, item, max)


///////////////////////////////////////////////////////
//#region OBJECT UPDATE update
///////////////////////////////////////////////////////

//@function Updates a Triangle's chart.point objects with new data.
export method update(Triangle this, chart.point a, chart.point b, chart.point c) =>
	if na(this.a)
		this.a := a
	else
		this.a.update(a)
	if na(this.b)
		this.b := b
	else
		this.b.update(b)
	if na(this.c)
		this.c := c
	else
		this.c.update(c)
	this

//@function Updates a Polygon's chart.point objects with new data.
export method update(Polygon this, chart.point[] points) =>
	if na(this.points)
		this.points := points
	else
		count = points.size()
		if this.points.size() != count
			this.points.clear()
			this.points.concat(points)
		else
			for [i,point] in points
				this.points.get(i).update(point)
	
	center = this.points.create_center()
	this.center := na(this.center) ? center : this.center.update(center)
	this


///////////////////////////////////////////////////////
//#endregion OBJECT UPDATE
///////////////////////////////////////////////////////

///////////////////////////////////////////////////////
//#region OBJECT REMOVAL delete
///////////////////////////////////////////////////////

safe_delete(this) =>
	if not na(this)
		this.plot.delete()
	this

safe_delete_items(this) =>
	if not na(this)
		for item in this
			item.delete()
	this

//@function Removes an objects representation from the chart.
export method delete(Triangle this) =>
	if not na(this)
		this.ab.delete()
		this.ac.delete()
		this.bc.delete()
	this

//@function Removes an objects representation from the chart.
export method delete(TriangleFill this) => 
	if not na(this)
		safe_delete(this.plot)
		this.triangle.delete()
	this

//@function Removes an objects representation from the chart.
export method delete(Polygon this) => safe_delete_items(this.plot)

//@function Removes an objects representation from the chart.
export method delete(PolygonFill this) => 
	if not na(this)
		this.poly.delete()
		if not na(this.plot)
			for item in this.plot
				item.delete()
	this

safe_delete_all(this) =>
	if not na(this)
		for item in this
			item.delete()
	this

//@function Removes the representations of an array of objects from the chart.
export method delete(Triangle[] this) => safe_delete_all(this)
//@function Removes the representations of an array of objects from the chart.
export method delete(TriangleFill[] this) => safe_delete_all(this)
//@function Removes the representations of an array of objects from the chart.
export method delete(Polygon[] this) => safe_delete_all(this)
//@function Removes the representations of an array of objects from the chart.
export method delete(PolygonFill[] this) => safe_delete_all(this)


///////////////////////////////////////////////////////
//#endregion OBJECT REMOVAL
///////////////////////////////////////////////////////

///////////////////////////////////////////////////////
//#region OBJECT TO BUILTIN draw
///////////////////////////////////////////////////////
init_args(this) =>
	if not na(this)
		this.args := this.args.or_default()
	this

//@function Adds/Updates the representations of an object to the chart.
export method draw(Triangle this, D.LineArgs ab_args_override = na, D.LineArgs ac_args_override = na, D.LineArgs bc_args_override = na) =>
	init_args(this)	
	if not na(this)
		this.ab.draw(extend_only = false)
		this.ac.draw(extend_only = false)
		this.bc.draw(extend_only = false)

		this.ab.apply_style(D.nz(ab_args_override, this.args))
		this.ac.apply_style(D.nz(ac_args_override, this.args))
		this.bc.apply_style(D.nz(bc_args_override, this.args))
	this

//@function Adds/Updates the representations of an object to the chart.
export method draw(Polygon this)=>
	init_args(this)
	if not na(this)
		this.plot.draw(extend_only = false)
		// for border in 
		// 	border.draw()		
	this

//@function Adds/Replaces the representations of the object on the chart.
export method draw(TriangleFill this)=>
	if not na(this)
		this.plot.draw()
	this

//@function Adds/Replaces the representations of the object on the chart. The border lines are drawn in order of the Polygon.points array
export method draw(PolygonFill this)=>
	var args_transp = D.LineArgs.new(#00000000)
	if not na(this)
		// no init, doesn't have args
		this.poly.center.update(this.poly.points.create_center()) // center point update
		if not na(this.plot)
			for segment in this.plot
				segment.triangle.draw(ac_args_override = args_transp, bc_args_override = args_transp)
				segment.draw()
	this

draw_all(id) =>
	for item in id
		item.draw()
	id

//@function converts this object into a builtin plot object, drawing it on the chart, plot objects will be recycled, if you want them to stay, make a copy
export method draw(Triangle[] this) => draw_all(this)
//@function converts this object into a builtin plot object, drawing it on the chart, plot objects will be recycled, if you want them to stay, make a copy
export method draw(TriangleFill[] this) => draw_all(this)
//@function converts this object into a builtin plot object, drawing it on the chart, plot objects will be recycled, if you want them to stay, make a copy
export method draw(Polygon[] this) => draw_all(this)
//@function converts this object into a builtin plot object, drawing it on the chart, plot objects will be recycled, if you want them to stay, make a copy
export method draw(PolygonFill[] this) => draw_all(this)
	
_apply_style(this, args) =>
	if not na(this)
		this.args := args
		this.plot.apply_style(args)
	this

//@function updates styles on this objects plot
export method apply_style(Triangle this, D.LineArgs args) =>
	if not na(this)
		this.args := args
		this.ab.apply_style(args)
		this.ac.apply_style(args)
		this.bc.apply_style(args)
	this

//@function updates styles on this objects plot
export method apply_style(Polygon this, D.LineArgs args) =>
	init_args(this)
	if not na(this)
		this.args := args
		if not na(this.plot)
			for l in this.plot //lines
				l.apply_style(args)
	this

_apply_style_all(this, args) =>
	if not na(this)
		for i in this
			i.apply_style(args)
	this

//@function updates styles on these objects' plots
export method apply_style(Triangle[] this, D.LineArgs args) => _apply_style_all(this, args)
//@function updates styles on these objects' plots
export method apply_style(Polygon[] this, D.LineArgs args) => _apply_style_all(this, args)

///////////////////////////////////////////////////////
//#endregion OBJECT TO BUILTIN
///////////////////////////////////////////////////////


/////////////////////////////////////////////////////////	
//					  	 DEMO	
/////////////////////////////////////////////////////////	
float hhp = ta.highest(high, 30)
float llp = ta.lowest(low, 30)
int hhi = ta.highestbars(high, 30)
int lli = ta.lowestbars(low, 30)
plot(bar_index, 'bi', display = display.data_window)
plot(hhp, 'hhp', display = display.data_window)
plot(llp, 'llp', display = display.data_window)
plot(hhi, 'hhi', display = display.data_window)
plot(lli, 'lli', display = display.data_window)

var chart.point p_dyn = chart.point.now(0)
var chart.point p1 = chart.point.now(1)
var chart.point p2 = chart.point.now(2)
var chart.point pt1 = chart.point.now(3)
var chart.point pt2 = chart.point.now(6)
var chart.point pt3 = chart.point.now(7)
var chart.point hh = chart.point.now(4)
var chart.point ll = chart.point.now(5)
var D.Line l1 = D.Line.new(p2, p1, D.LineArgs.new(color.fuchsia))
var D.Line l2 = D.Line.new(hh, ll, D.LineArgs.new(color.purple))


if barstate.islast
	p_dyn.update(chart.point.now())
	hh.update(chart.point.new(time[-hhi], bar_index[-hhi], hhp))	
	ll.update(chart.point.new(time[-lli], bar_index[-lli], llp))
	p1.update(time[0], bar_index[0], high[0])
	pt1.update(time[10], bar_index[10], high[10] + 3*ta.tr)
	pt2.update(time[5], bar_index[5], low[5] - 3*ta.tr)
	pt3.update(time[15], bar_index[15], low[15] - 3*ta.tr)
	p2.update(time[20], bar_index[20], high[20])
	chart.point p3 = chart.point.new(time[30], bar_index[30], high[30])
	chart.point p4 = chart.point.new(time[40], bar_index[40], high[40])
	var fix3 = p3.copy()
	var fix4 = p4.copy()

	var D.Label l_hh = hh.create_label('HH')
	var D.Label l_ll = ll.create_label('LL', D.LabelArgs.new(style = label.style_label_up))
	var D.Label l_p1 = p1.create_label('p1')
	var D.Label l_pt1 = pt1.create_label('pt1')
	var D.Label l_pt2 = pt2.create_label('pt2', D.LabelArgs.new(style = label.style_label_up))
	var D.Label l_pt3 = pt3.create_label('pt3', D.LabelArgs.new(style = label.style_label_up))
	var D.Label l_p2 = p2.create_label('p2')
	var D.Label l_p3 = p3.create_label('p3')
	var D.Label l_p4 = p4.create_label('p4')
	var D.Label l_fixp3 = fix3.create_label('fix 3').draw() // draw once
	var D.Label l_fixp4 = fix4.create_label('fix 4').draw() // draw once
	
	l_hh.draw()
	l_ll.draw()
	l_p1.draw() // label updated indirect, still same point, but point that was updated
	l_pt1.draw() // label updated indirect, still same point, but point that was updated
	l_pt2.draw() // label updated indirect, still same point, but point that was updated
	l_pt3.draw() // label updated indirect, still same point, but point that was updated
	l_p2.draw() // label updated indirect, still same point, but point that was updated
	l_p3.update(p3).draw() // update label with new point
	l_p4.update(p4).draw() // update label with new point


	l1.draw(extend_only = false) // based on instances p1 and p2, which are updated, hence l1.plot is updated as well


	// var Triangle t1 = p2.create_triangle(p3, p4) // expanding triangle with two fix point p4 and two updating points p2,p3,  since the plotted triangle lines l1 and l2 are updated because they have chart.point p2 as member, which is updated, the fill will also update
	// t1.draw()
	// var D.Label l_t1 = t1.create_label('triangle')
	// l_t1.draw()

	var Triangle t2 = l1.create_triangle(pt1)
	t2.draw() // fix triangle, created once, with l2 and ll, but only drawn once at init of var
	var D.CenterLabel l_t2 = t2.create_center_label('triangle 2', D.LabelArgs.new(style = label.style_label_center))
	l_t2.draw()
	var TriangleFill tf_te2 = t2.create_fill(#ff525240)
	tf_te2.draw()


	var Polygon p = array.from(p1, pt2, pt3, p2).create_polygon()
	p.draw()
	var PolygonFill pf = p.create_fill(#ff99003e)
	pf.draw()
	var D.CenterLabel l_pc = p.create_label('poly')
	l_pc.draw()

