lib_plot_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

// @description library wrapping basic builtin object constructors, to be able to do calculations with points/lines/boxes/triangles/polygons via libraries and on securities. inspired by Trendoscope's (https://www.tradingview.com/script/63c8VXSa-DrawingTypes/ and https://www.tradingview.com/script/eNM7BFaR-DrawingMethods/) with added update mechanism to not have to recreate objects on every iteration for continously drawn items, automated xloc selection for coordinates, compatibility check for chart.points, added Triangle and Polygon types, object reflection via tostring to valid json (logging via webhook)
library("lib_plot_objects", overlay = true)

///////////////////////////////////////////////////////
//#region BUILTIN OBJECT WRAPPERS structures
///////////////////////////////////////////////////////

//@description candle data
//@field bar_time time based x coordinate
//@field bar_time_close time based x coordinate for closing time
//@field bar_idx bar index based x coordinate
//@field o candle open price
//@field h candle highest price
//@field l candle lowest price
//@field c candle close price
//@field v candle volume
export type Candle
	int bar_time = time
	int bar_time_close = time_close
	int bar_idx = bar_index
	float o = open
	float h = high
	float l = low
	float c = close
	float v = volume

//@description Reusable arguments for label.new()
//@field text_font_family The font family of the text. Optional. The default value is font.family_default. Possible values: font.family_default, font.family_monospace.
//@field xloc See description of x argument. Possible values: xloc.bar_index and xloc.bar_time. Default is xloc.bar_index.
//@field yloc Possible values are yloc.price, yloc.abovebar, yloc.belowbar. If yloc=yloc.price, y argument specifies the price of the label position. If yloc=yloc.abovebar, label is located above bar. If yloc=yloc.belowbar, label is located below bar. Default is yloc.price.
//@field bg_color Color of the label border and arrow.
//@field style Label style. Possible values: label.style_none, label.style_xcross, label.style_cross, label.style_triangleup, label.style_triangledown, label.style_flag, label.style_circle, label.style_arrowup, label.style_arrowdown, label.style_label_up, label.style_label_down, label.style_label_left, label.style_label_right, label.style_label_lower_left, label.style_label_lower_right, label.style_label_upper_left, label.style_label_upper_right, label.style_label_center, label.style_square, label.style_diamond, label.style_text_outline. Default is label.style_label_down.
//@field text_color Text color.
//@field size Label size. Possible values: size.auto, size.tiny, size.small, size.normal, size.large, size.huge. Default value is size.normal.
//@field text_align Label text alignment. Possible values: text.align_left, text.align_center, text.align_right. Default value is text.align_center.
export type LabelArgs
	color text_color = chart.fg_color
	color bg_color = #00000000
	string text_font_family = font.family_default
	string xloc = xloc.bar_index
	string yloc = yloc.price
	string style = label.style_label_down
	string size = size.normal
	string text_align = text.align_center


//@description Wrapper for builtin label
//@field point The Label coordinates
//@field txt Label text. Default is empty string.
//@field args Wrapper for reusable arguments for label.new()
//@field tooltip Hover to see tooltip label.
//@field plot The label object to be added and plotted via draw()
export type Label
	chart.point point
	string txt = ''
	LabelArgs args = na
	string tooltip = na
	label plot = na


//@description Wrapper for Label centered between an array of chart.points
//@field points The coordinates around the center
//@field center_label The generated, centered Label
export type CenterLabel
	chart.point[] points
	Label center_label


//@description Reusable arguments for line.new()
//@field line_color Line color.
//@field style Line style. Possible values: line.style_solid, line.style_dotted, line.style_dashed, line.style_arrow_left, line.style_arrow_right, line.style_arrow_both.
//@field width Line width in pixels.
//@field xloc Possible values: xloc.bar_index and xloc.bar_time. Default is xloc.bar_index.
//@field extend f extend=extend.none, draws segment starting at point (x1, y1) and ending at point (x2, y2). If extend is equal to extend.right or extend.left, draws a ray starting at point (x1, y1) or (x2, y2), respectively. If extend=extend.both, draws a straight line that goes through these points. Default value is extend.none.
export type LineArgs
	color line_color = chart.fg_color
	string style = line.style_solid
	int width = 1
	string xloc = xloc.bar_index
	string extend = extend.none

//@description Wrapper for builtin line
//@field start starting point of the line
//@field start end point of the line
//@field args Wrapper for reusable arguments for line.new()
//@field plot The line object to be added and plotted via draw()
export type Line
	chart.point start
	chart.point end
	LineArgs args
	line plot = na


//@description Creates a new linefill object and displays it on the chart, filling the space between a and b with the color specified in fill_color.
//@field a The first Line object
//@field b The second Line 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 LineFill
	Line a
	Line b 
	color fill_color = #dededede
	bool needs_support = false
	Line support = na
	linefill plot = na
	

//@description Reusable arguments for box.new()
//@field border_color Color of the four borders. Optional. The default is color.blue.
//@field border_width Width of the four borders, in pixels. Optional. The default is 1 pixel.
//@field border_style Style of the four borders. Possible values: line.style_solid, line.style_dotted, line.style_dashed. Optional. The default value is line.style_solid.
//@field bg_color Background color of the box. Optional. The default is color.blue.
//@field xloc Determines whether the arguments to 'left' and 'right' are a bar index or a time value. If xloc = xloc.bar_index, the arguments must be a bar index. If xloc = xloc.bar_time, the arguments must be a UNIX time. Possible values: xloc.bar_index and xloc.bar_time. Optional. The default is xloc.bar_index.
//@field extend When extend.none is used, the horizontal borders start at the left border and end at the right border. With extend.left or extend.right, the horizontal borders are extended indefinitely to the left or right of the box, respectively. With extend.both, the horizontal borders are extended on both sides. Optional. The default value is extend.none.
export type BoxArgs
	color border_color = chart.fg_color
	int border_width = 1
	string border_style = line.style_solid
	color bg_color = #dededede
	string xloc = xloc.bar_index
	string extend = extend.none


//@description Reusable text arguments for box.new()
//@field text_size The size of the text. An optional parameter, the default value is size.auto. Possible values: size.auto, size.tiny, size.small, size.normal, size.large, size.huge.
//@field text_font_family The font family of the text. Optional. The default value is font.family_default. Possible values: font.family_default, font.family_monospace.
//@field text_color The color of the text. Optional. The default is color.black.
//@field text_halign The horizontal alignment of the box's text. Optional. The default value is text.align_center. Possible values: text.align_left, text.align_center, text.align_right.
//@field text_valign The vertical alignment of the box's text. Optional. The default value is text.align_center. Possible values: text.align_top, text.align_center, text.align_bottom.
//@field text_wrap Defines whether the text is presented in a single line, extending past the width of the box if necessary, or wrapped so every line is no wider than the box itself (and clipped by the bottom border of the box if the height of the resulting wrapped text is higher than the height of the box). Optional. The default value is text.wrap_none. Possible values: text.wrap_none, text.wrap_auto.
export type BoxTextArgs
	color text_color = chart.fg_color
	string text_size = size.normal
	string text_halign = text.align_center
	string text_valign = text.align_center
	string text_wrap = text.wrap_none
	string text_font_family = font.family_default


//@description Wrapper for builtin box
//@field left_top top-left corner of the box 
//@field right_bottom bottom-right corner of the box
//@field txt The text to be displayed inside the box. Optional. The default is empty string.
//@field args Wrapper for reusable arguments for box.new()
//@field plot The box object to be added and plotted via draw()
export type Box
	chart.point left_top
	chart.point right_bottom
	string txt = ''
	BoxArgs args = na
	BoxTextArgs text_args = na
	box plot = na

///////////////////////////////////////////////////////
//#endregion BUILTIN OBJECT WRAPPERS
///////////////////////////////////////////////////////


///////////////////////////////////////////////////////
//#region SETTERS
///////////////////////////////////////////////////////

export method set_line_color(LineArgs this, color value) =>
    this.line_color := value
    this
export method set_style(LineArgs this, string style) =>
    this.style := style
    this
export method set_width(LineArgs this, int width) =>
    this.width := width
    this
export method set_extend(LineArgs this, string extend) =>
    this.extend := extend
    this

export method set_txt_color(LabelArgs this, color value) =>
    this.text_color := value
    this
export method set_bg_color(LabelArgs this, color value) =>
    this.bg_color := value
    this
export method set_style(LabelArgs this, string value) =>
    this.style := value
    this
export method set_yloc(LabelArgs this, string yloc) =>
    this.yloc := yloc
    this
export method set_size(LabelArgs this, string value) =>
    this.size := value
    this



export method extend_to(Line this, int end_time, int end_idx, float end_price = na) =>
	if not na(this)
		this.end.time := end_time
		this.end.index := end_idx
		if not na(end_price)
			this.end.price := end_price
	this

///////////////////////////////////////////////////////
//#endregion SETTERS
///////////////////////////////////////////////////////


///////////////////////////////////////////////////////
//#region HELPER METHODS
///////////////////////////////////////////////////////

//@function checks args oject for being na, if so, provide a default instead
export method or_default(LineArgs args) => na(args) ? LineArgs.new() : args
//@function checks args oject for being na, if so, provide a default instead
export method or_default(LabelArgs args) => na(args) ? LabelArgs.new() : args
//@function checks args oject for being na, if so, provide a default instead
export method or_default(BoxArgs args) => na(args) ? BoxArgs.new() : args
//@function checks args oject for being na, if so, provide a default instead
export method or_default(BoxTextArgs args) => na(args) ? BoxTextArgs.new() : args

export method direction(Candle this) =>
	this.o <= this.c ? 1 : -1
export method is_green(Candle this) =>
	this.direction() == 1
export method is_red(Candle this) =>
	this.direction() == -1
export method is_bullish(Candle this) =>
	this.direction() == 1
export method is_bearish(Candle this) =>
	this.direction() == -1
export method total_size(Candle this) =>
	this.h - this.l
export method body_size(Candle this) =>
	math.abs(this.o - this.c)
export method body_top(Candle this) =>
	math.max(this.o, this.c)	
export method body_bottom(Candle this) =>
	math.min(this.o, this.c)
export method wick_size_top(Candle this) =>
	this.h - this.body_top()	
export method wick_size_bottom(Candle this) =>
	this.body_bottom() - this.l
export method body_total_ratio(Candle this) =>
	this.body_size() / this.total_size()
export method duration(Candle this) =>
	this.bar_time_close - this.bar_time




///////////////////////////////////////////////////////
//#endregion HELPER METHODS
///////////////////////////////////////////////////////


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


//@function converts object to json representation
export method tostring(LineArgs this) =>
	na(this)?'null':'{' + str.format('"style": {0}, "width": {1}, "extend": {2}',  this.style, this.width, this.extend) + '}'

//@function converts object to json representation
export method tostring(LabelArgs this) =>
	na(this)?'null':'{' + str.format('"text_font_family": "{0}", "yloc": "{1}", "style": "{2}", "size": "{3}", "text_align": "{4}"', this.text_font_family, this.yloc, this.style, this.size, this.text_align) + '}'

//@function converts object to json representation
export method tostring(BoxArgs this) =>
	na(this)?'null':'{' + str.format('"border_style": {0}, "border_width": {1}, "extend": {2}',  this.border_style, this.border_width, this.extend) + '}'

//@function converts object to json representation
export method tostring(BoxTextArgs this) =>
	na(this)?'null':'{' + str.format('"text_font_family": "{0}", "text_valign": "{1}", "text_halign": "{2}", "text_size": "{3}", "text_wrap": "{4}"',  this.text_font_family, this.text_valign, this.text_halign, this.text_size, this.text_wrap) + '}'

//@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(Candle this, simple string date_format = 'dd-MM-y HH:mm') =>
	na(this)?'null':'{' + str.format('"bar_idx": {0}, "bar_time": "{1,date,'+date_format+'}", "o": {2}, "h": {3}, "l": {4}, "c": {5}, "v": {6}, "bar_time_close": "{7,date,'+date_format+'}"',  this.bar_idx, this.bar_time, this.o, this.h, this.l, this.c, this.v, this.bar_time_close) + '}'

//@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(chart.point this, simple string date_format = 'dd-MM-y HH:mm') =>
	na(this)?'null':'{' + str.format('"bar_idx": {0}, "bar_time": "{1,date,'+date_format+'}", "price": {2}', this.index, this.time, this.price) + '}'

//@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(chart.point[] 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 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(Line this, simple string date_format = 'dd-MM-y HH:mm') =>
	na(this)?'null':'{' + str.format('"start": {0}, "end": {1}, "args": {2}', this.start.tostring(date_format), this.end.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(Label this, simple string date_format = 'dd-MM-y HH:mm') =>
	na(this)?'null':'{' + str.format('"point": {0}, "txt": "{1}", "args": {2}', this.point.tostring(date_format), this.txt, 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(LineFill this, simple string date_format = 'dd-MM-y HH:mm') =>
	na(this)?'null':'{' + str.format('"a": {0}, "b": {1}, "needs_support" : {2}, "support" : {3}', this.a.tostring(date_format), this.b.tostring(date_format), this.needs_support, this.support.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(Box this, simple string date_format = 'dd-MM-y HH:mm') =>
	na(this)?'null':'{' + str.format('"left_top": {0}, "right_bottom": {1}, "text": "{2}", "args": {3}, "text_args": {4}', this.left_top.tostring(date_format), this.right_bottom.tostring(date_format), this.txt, this.args.tostring(), this.text_args.tostring()) + '}'

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(', ') + ']'


tostrings_args(this) =>
	strings = array.new<string>(this.size())
	for [i,item] in this
		strings.set(i, tostring(item))
	'[' + strings.join(', ') + ']'


//@function converts array of objecta to json representation
export method tostring(LineArgs[] this) => tostrings_args(this)
//@function converts array of objecta to json representation
export method tostring(LabelArgs[] this) => tostrings_args(this)
//@function converts array of objecta to json representation
export method tostring(BoxArgs[] this) => tostrings_args(this)
//@function converts array of objecta to json representation
export method tostring(BoxTextArgs[] this) => tostrings_args(this)
//@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(Candle[] 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(Line[] 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(Label[] 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(Box[] 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
///////////////////////////////////////////////////////


// Candle
// @function Creates Candle with ohlc and index data
// @param t bar time of this candle
// @param tc bar close time of this candle
// @param idx bar index of this candle
// @param o open price of this candle
// @param h highest price of this candle
// @param l lowest price of this candle
// @param c close price of this candle
// @returns new Candle
export create_candle(int t = time, int tc = time_close, int idx = bar_index, float o = open, float h = high, float l = low, float c = close, float v = volume) =>
	Candle.new(t, tc, idx, o, h, l, c, v)

// chart.points
// @function Workaround for chart.point.new/now() returning the same instance when being fed equal parameters. This function only creates a new instance when id's match (as long as this behaviour exists)
// @param this chart.point
// @param other chart.point
// @returns center chart.point
export create_point_var(int id, int bar_time = time, int bar_idx = bar_index, float price = close) =>
	point = chart.point.new(id, bar_idx, price)
	point.time := bar_time
	point

// @function Creates center chart.point between two chart.points
// @param this chart.point
// @param other chart.point
// @returns center chart.point
export method create_center(chart.point this, chart.point other) =>
	if not na(this) and not na(other)
		center_bar_idx = int(math.avg(this.index, other.index))
		center_bar_time = time[bar_index - center_bar_idx]
		center_price = other.price + (this.price - other.price) * (center_bar_idx - other.index) / (this.index - other.index) 
		chart.point.new(center_bar_time, center_bar_idx, center_price)
	else
		not na(this) ? this : not na(other) ? other : chart.point.now()

// @function Creates center chart.point between an array of given chart.points
// @param points array of chart.points for calculation of the center
// @returns center chart.point
export method create_center(chart.point[] points) =>
	count = points.size()
	bar_sum = 0
	price_sum = 0.0
	center_xloc = xloc.bar_index
	chart.point center_point = na

	if count == 2
		center_point := create_center(points.get(0), points.get(1))

	else
		for point in points
			if not na(point)
				bar_sum += point.index
				price_sum += point.price

		center_bar_idx = int(bar_sum/count)
		center_bar_time = time[bar_index - center_bar_idx]
		center_price = price_sum / count
		center_point := chart.point.new(center_bar_time, center_bar_idx, center_price)
	
	center_point

export method create_center(Line this) =>
	this.start.create_center(this.end)

// Labels
//@function Creates a new Label object.
export method create_label(chart.point this, string txt = na, LabelArgs args = na, string tooltip = na) =>
	if not na(this)
		Label.new(this, txt, args.or_default(), tooltip)

//@function Creates a new Label object.
export method create_label(Line this, string txt = na, LabelArgs args = na, string tooltip = na) =>
	if not na(this)
		Label.new(this.start.create_center(this.end), txt, args.or_default(), tooltip)


//@function Creates a new Label object.
export method create_label(Box this, string txt = na, LabelArgs args = na, string tooltip = na) =>
	if not na(this)
		Label.new(this.left_top.create_center(this.right_bottom), txt, args.or_default(), tooltip)

//@function Creates a new Label object.
export method create_center_label(Line this, string txt = na, LabelArgs args = na, string tooltip = na) =>
	points = array.from(this.start, this.end)
	center_label = this.create_center().create_label(txt, args.or_default(), tooltip)
	CenterLabel.new(points, center_label)


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

export method nz(chart.point this,  chart.point default = na) 		=>	not na(default) ? _nz(this, default) : _nz(this, chart.point.now())
export method nz(Candle this, 		Candle default = na) 			=>	not na(default) ? _nz(this, default) : _nz(this, Candle.new())
export method nz(LabelArgs this, 	LabelArgs default = na) 		=>	not na(default) ? _nz(this, default) : _nz(this, LabelArgs.new())
export method nz(Label this, 		Label default = na)		 		=>	not na(default) ? _nz(this, default) : _nz(this, Label.new(chart.point.now()))
export method nz(LineArgs this, 	LineArgs default = na) 			=>	not na(default) ? _nz(this, default) : _nz(this, LineArgs.new())
export method nz(Line this, 		Line default = na)	 			=>	not na(default) ? _nz(this, default) : _nz(this, Line.new(chart.point.now(), chart.point.now(), LineArgs.new()))
export method nz(CenterLabel this, 	CenterLabel default = na)		=>	not na(default) ? _nz(this, default) : _nz(this, Line.new(chart.point.now(), chart.point.now(), LineArgs.new()).create_center_label('', LabelArgs.new()))
export method nz(LineFill this, 	LineFill default = na) 			=>	not na(default) ? _nz(this, default) : _nz(this, LineFill.new(Line.new(chart.point.now(1), chart.point.now(2), LineArgs.new()),Line.new(chart.point.now(3), chart.point.now(4), LineArgs.new())))
export method nz(BoxArgs this, 		BoxArgs default = na) 			=>	not na(default) ? _nz(this, default) : _nz(this, BoxArgs.new())
export method nz(BoxTextArgs this, 	BoxTextArgs default = na) 		=>	not na(default) ? _nz(this, default) : _nz(this, BoxTextArgs.new())
export method nz(Box this, 			Box default = na)	 			=>	not na(default) ? _nz(this, default) : _nz(this, Box.new(chart.point.now(3), chart.point.now(3), '', BoxArgs.new(), BoxTextArgs.new()))


// Lines

export method create_line(chart.point this, chart.point other, LineArgs args = na) =>
	Line.new(this, other, na(args) ? LineArgs.new() : args)

// Boxes

export method create_box(chart.point this, chart.point other, string txt = na, BoxArgs args = na, BoxTextArgs text_args = na) =>
	Box.new(this, other, txt, na(args) ? BoxArgs.new() : args, na(text_args) ? BoxTextArgs.new() : text_args)

export method create_box(Line this, string txt = na, BoxArgs args = na, BoxTextArgs text_args = na) =>
	Box.new(this.start, this.end, txt, na(args) ? BoxArgs.new() : args, na(text_args) ? BoxTextArgs.new() : text_args)


// LineFill
//@function Creates a new LineFill object.
//@field this The first Line object
//@field other The second Line object
//@field fill_color The color used to fill the space between the lines.
//@field support Optional support line if this.end == other.start, if that's the case and line is not passed as parameter, a new one is created
export method create_fill(Line this, Line other, color fill_color = #dededede, Line support = na) =>
	if na(this) or na(other) ? false : not na(this.start) and not na(this.end) and not na(other.start) and not na(other.end)
		var LineArgs transp = LineArgs.new(#00000000)
		needs_support = this.start.index == other.end.index or this.end.index == other.start.index
		LineFill.new(this, other, fill_color, needs_support, needs_support ? nz(support, Line.new(other.end, other.start, transp)) : na)

export create_line(int start_time, int start_idx, float start_price, int end_time, int end_idx, float end_price, LineArgs args = na) =>
	start = chart.point.new(start_time, start_idx, start_price)
	end = chart.point.new(end_time, end_idx, end_price)
	Line.new(start, end, args.nz())

export create_box(int left_time, int left_idx, float top, int right_time, int right_idx, float bottom, string txt = na, BoxArgs args = na, BoxTextArgs text_args = na) =>
	left_top = chart.point.new(left_time, left_idx, top)
	right_bottom = chart.point.new(right_time, right_idx, bottom)
	Box.new(left_top, right_bottom, txt, args.nz(), text_args.nz())


///////////////////////////////////////////////////////
//#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(Candle[] id, Candle item, int max = 0) => _enqueue(id, item, max)
export method enqueue(chart.point[] id, chart.point item, int max = 0) => _enqueue(id, item, max)
export method enqueue(Label[] id, Label item, int max = 0) => _enqueue(id, item, max)
export method enqueue(CenterLabel[] id, CenterLabel item, int max = 0) => _enqueue(id, item, max)
export method enqueue(Line[] id, Line item, int max = 0) => _enqueue(id, item, max)
export method enqueue(LineFill[] id, LineFill item, int max = 0) => _enqueue(id, item, max)


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


//@function Updates a Candle object with new data.
export method update(Candle this, int bar_time = time, int bar_time_close = time, int bar_idx = bar_index, float o = open, float h = high, float l = low, float c = close, float v = volume) =>
	this.bar_time := bar_time
	this.bar_time_close := bar_time_close
	this.bar_idx := bar_idx
	this.o := o
	this.h := h
	this.l := l
	this.c := c
	this.v := v
	this


//@function Updates a chart.point object with new data.
export method update(chart.point this, int bar_time = time, int bar_idx = bar_index, float price = close) =>
	if not na(this)
		this.time := bar_time
		this.index := bar_idx
		this.price := price
	this

//@function Updates a chart.point object with new data from another chart.point.
export method update(chart.point this, chart.point update) =>
	if na(update)
		runtime.error('chart.point.update parameter must not be na')
	this.update(update.time, update.index, update.price)

//@function Updates a Label's chart.point object with new data from another chart.point, or sets it if the Label's chart.point is na. If txt parameter is not given, it's not updated.
export method update(Label this, chart.point point) =>
	if na(this.point)
		this.point := point
	else
		this.point.update(point)
	this

//@function Updates a Label's chart.point object with new data from another chart.point, or sets it if the Label's chart.point is na. If txt parameter is not given, it's not updated.
export method update(Label this, chart.point point, string txt) =>
	this.update(point)
	this.txt := txt
	this

//@function Updates a Label's chart.point object with new data from another chart.point, or sets it if the Label's chart.point is na. If txt parameter is not given, it's not updated.
export method update(CenterLabel this, chart.point[] points) =>
	center = points.create_center()
	this.points := points
	this.center_label.update(center)
	this


//@function Updates a Label's chart.point object with new data from another chart.point, or sets it if the Label's chart.point is na. If txt parameter is not given, it's not updated.
export method update(CenterLabel this, chart.point[] points, string txt) =>
	this.update(points)
	this.center_label.txt := txt
	this

//@function Updates a Label's chart.point object with new data from another chart.point, or sets it if the Label's chart.point is na. If txt parameter is not given, it's not updated.
export method update(CenterLabel this, Line l) =>
	center = l.create_center()
	this.points := array.from(l.start, l.end)
	this.center_label.update(center)
	this

//@function Updates a Label's chart.point object with new data from another chart.point, or sets it if the Label's chart.point is na. If txt parameter is not given, it's not updated.
export method update(CenterLabel this, Line l, string txt) =>
	this.update(l)
	this.center_label.txt := txt
	this
	
//@function Updates this Line's chart.point objects with new data from other chart.points.
export method update(Line this, chart.point start, chart.point end) =>
	if na(this.start)
		this.start := start
	else
		this.start.update(start)
	if na(this.end)
		this.end := end
	else
		this.end.update(end)
	this



//@function Updates a Box's chart.point objects with new data from other chart.points.
export method update(Box this, chart.point left_top, chart.point right_bottom) =>
	if na(this.left_top)
		this.left_top := left_top
	else
		this.left_top.update(left_top)
	if na(this.right_bottom)
		this.right_bottom := right_bottom
	else
		this.right_bottom.update(right_bottom)
	this


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

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

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

//@function Removes an objects representation from the chart.
export method delete(Line this) => safe_delete(this)
//@function Removes an objects representation from the chart.
export method delete(Label this) => safe_delete(this)
//@function Removes an objects representation from the chart.
export method delete(CenterLabel this) => safe_delete(this.center_label)
//@function Removes an objects representation from the chart.
export method delete(Box this) => safe_delete(this)
//@function Removes an objects representation from the chart.
export method delete(LineFill this) => 
	safe_delete(this)
	this.support.delete()

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 (line[] this) => safe_delete_all(this)
//@function Removes the representations of an array of objects from the chart.
export method delete (label[] this) => safe_delete_all(this)
//@function Removes the representations of an array of objects from the chart.
export method delete (box[] this) => safe_delete_all(this)
//@function Removes the representations of an array of objects from the chart.
export method delete (linefill[] this) => safe_delete_all(this)
//@function Removes the representations of an array of objects from the chart.
export method delete(Line[] this) => safe_delete_all(this)
//@function Removes the representations of an array of objects from the chart.
export method delete(Label[] this) => safe_delete_all(this)
//@function Removes the representations of an array of objects from the chart.
export method delete(CenterLabel[] this) => safe_delete_all(this)
//@function Removes the representations of an array of objects from the chart.
export method delete(LineFill[] this) => safe_delete_all(this)
//@function Removes the representations of an array of objects from the chart.
export method delete(Box[] this) => safe_delete_all(this)
//@function Removes the representations of an array of objects from the chart.


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

///////////////////////////////////////////////////////
//#region OBJECT TO BUILTIN draw
///////////////////////////////////////////////////////

method x(chart.point this, string xloc = xloc.bar_index) =>
	if na(this)
		na
	else if xloc == xloc.bar_time
		this.time
	else
		this.index


init_args(this) =>
	if not na(this)
		this.args := this.args.or_default()

//@function Adds/Updates the representations of an object to the chart.
//@param extend_only this will omit redrawing x1 of the line, thereby prevent adressing bar_indexes too far in the past which causes max_bars_back errors. (default: true, set to false if you want x1 updated on drawing)
export method draw(line this, chart.point a, chart.point b, LineArgs args, bool extend_only = true) =>
	l = na(this) and not na(a) and not na(b) ? line.new(a, b, args.xloc, args.extend, args.line_color, args.style, args.width) : this

	if not na(this)
		if extend_only
			l.set_y1(a.price)
		else
			l.set_first_point(a)
		l.set_second_point(b)
		
	l

//@function Adds/Updates the representations of an object to the chart.
export method draw(Label this) =>
	init_args(this)
	if not na(this)
		this.plot := na(this.plot) ? label.new(this.point, this.txt, this.args.xloc, this.args.yloc, this.args.bg_color, this.args.style, this.args.text_color, this.args.size, this.args.text_align, this.tooltip) : this.plot	
		this.plot.set_point(this.point)
		this.plot.set_text(this.txt)
		this.plot.set_tooltip(this.tooltip)
	this

//@function Adds/Updates the representations of an object to the chart.
export method draw(CenterLabel this) =>
	if not na(this)
		this.center_label.update(this.points.create_center())
		this.center_label.draw()
	this

//@function Adds/Updates the representations of an object to the chart.
//@param extend_only this will omit redrawing x1 of the line, thereby prevent adressing bar_indexes too far in the past which causes max_bars_back errors. (default: true, set to false if you want x1 updated on drawing)
export method draw(Line this, bool extend_only = true) =>
	init_args(this)
	if not na(this)
		this.plot := this.plot.draw(this.start, this.end, this.args, extend_only)
	this


//@function Adds/Updates the representations of an object to the chart.
//@param extend_only this will omit redrawing the left side of the box, thereby prevent adressing bar_indexes too far in the past which causes max_bars_back errors. (default: true, set to false if you want x1 updated on drawing)
export method draw(Box this, bool extend_only = true) =>
	this.plot := na(this.plot) ? box.new(this.left_top, this.right_bottom,  this.args.border_color, this.args.border_width, this.args.border_style, this.args.extend, this.args.xloc, this.args.bg_color, this.txt, this.text_args.text_size, this.text_args.text_color, this.text_args.text_halign, this.text_args.text_valign, this.text_args.text_wrap, this.text_args.text_font_family) : this.plot
	if extend_only
		this.plot.set_top(this.left_top.price)
	else
		this.plot.set_top_left_point(this.left_top)
	this.plot.set_bottom_right_point(this.right_bottom)
	this


//@function Adds/Replaces the representations of the object on the chart.
export method draw(LineFill this)=>
	// no init, doesn't have args
	if not na(this)
		this.plot := na(this.plot) ? linefill.new(this.a.plot, this.needs_support and not na(this.support) ? this.support.draw().plot : this.b.plot, this.fill_color) : this.plot
		if this.needs_support and not na(this.support)
			this.support.draw()
	this

draw_all(id) =>
	for item in id
		item.draw()
	id
draw_all(id, extend_only) =>
	for item in id
		item.draw(extend_only)
	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(Label[] 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(CenterLabel[] 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(Line[] this, bool extend_only = true) => draw_all(this, extend_only)
//@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(LineFill[] 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(Box[] this, bool extend_only = true) => draw_all(this, extend_only)
//@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

_hide(chart.point point, drawable) =>
	point.update(na,na,na)
	drawable.draw()

//@function hides an object by setting it's coordinates na, to unhide just update coordinates and call draw() again
export method hide(Label this) => _hide(this.point, this)
//@function hides an object by setting it's coordinates na, to unhide just update coordinates and call draw() again
export method hide(CenterLabel this) => _hide(this.center_label.point, this)
//@function hides an object by setting it's coordinates na, to unhide just update coordinates and call draw() again
export method hide(Line this) => _hide(this.end, this)
//@function hides an object by setting it's coordinates na, to unhide just update coordinates and call draw() again
export method hide(Box this) => _hide(this.right_bottom, this)

export method apply_style(line this, LineArgs args) =>
	if not na(this) and not na(args)
		this.set_color(args.line_color)
		this.set_style(args.style)

export method apply_style(label this, LabelArgs args) =>
	if not na(this) and not na(args)
		this.set_textcolor(args.text_color)
		this.set_color(args.bg_color)
		this.set_style(args.style)
		
export method apply_style(box this, BoxArgs args, BoxTextArgs text_args) =>
	if not na(this)
		if not na(args)
			this.set_border_color(args.border_color)
			this.set_border_style(args.border_style)
			this.set_bgcolor(args.bg_color)
		if not na(text_args)
			this.set_text_color(text_args.text_color)
	
_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(Label this, LabelArgs args) => _apply_style(this, args)
export method apply_style(CenterLabel this, LabelArgs args) => _apply_style(this.center_label, args)
export method apply_style(Line this, LineArgs args) => _apply_style(this, args)

//@function updates styles on this objects plot
export method apply_style(Box this, BoxArgs args, BoxTextArgs text_args) => 
	if not na(this)
		this.args := args
		this.text_args := text_args
		this.plot.apply_style(args, text_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(label[] this, LabelArgs args) => _apply_style_all(this, args)
//@function updates styles on these objects' plots
export method apply_style(Label[] this, LabelArgs args) => _apply_style_all(this, args)
//@function updates styles on these objects' plots
export method apply_style(CenterLabel[] this, LabelArgs args) => _apply_style_all(this, args)
//@function updates styles on these objects' plots
export method apply_style(line[] this, LineArgs args) => _apply_style_all(this, args)
//@function updates styles on these objects' plots
export method apply_style(Line[] this, LineArgs args) => _apply_style_all(this, args)
//@function updates styles on these objects' plots
export method apply_style(Box[] this, BoxArgs args, BoxTextArgs text_args) =>
	if not na(this)
		for i in this
			i.apply_style(args, text_args)
	this

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


/////////////////////////////////////////////////////////	
//					  	 DEMO	
/////////////////////////////////////////////////////////	

chart.point now = chart.point.now()
plot(now.time, 'point time', display = display.data_window)
plot(now.index, 'point bar index', display = display.data_window)
plot(now.price, 'point price', display = display.data_window)

float hhp = ta.highest(high, 30)
float llp = ta.lowest(low, 30)
int hhi = ta.highestbars(high, 30)
int lli = ta.lowestbars(low, 30)

// HACK via differing price, otherwise function returns same instance for all points
var chart.point p1   = chart.point.now(2)
var chart.point p2   = chart.point.now(3)
var chart.point p3   = chart.point.now(4)
var chart.point p4   = chart.point.now(5)
var chart.point hh   = chart.point.now(6)
var chart.point hhe  = chart.point.now(7)
var chart.point ll   = chart.point.now(8)
var chart.point lle  = chart.point.now(9)

var Label l_hh = hh.create_label('hh', LabelArgs.new())
var Label l_ll = ll.create_label('ll', LabelArgs.new(style = label.style_label_up))
var Label l_p1 = p1.create_label('p1')
var Label l_p2 = p2.create_label('p2')
var Label l_p3 = p3.create_label('p3')
var Label l_p4 = p4.create_label('p4')

var Line lhh = Line.new(hh, hhe, LineArgs.new(color.red))
var Line lll = Line.new(ll, lle, LineArgs.new(color.green))
var Line l2 = na // (p3, p2)

// // single point update
// var chart.point latest_point = now.copy()
// var Label latest = latest_point.create_label('latest').draw()
// latest_point.update()
// latest.draw()

if barstate.islast

	p1.update(time[0], bar_index[0], high[0])
	p2.update(time[20], bar_index[20], high[20])
	p3.update(time[30], bar_index[30], high[30])
	p4.update(time[40], bar_index[40], high[40])

	hh.update(time[-hhi], bar_index[-hhi], hhp)
	ll.update(time[-lli], bar_index[-lli], llp)
	hhe.update(time, bar_index, hhp)
	lle.update(time, bar_index, llp)

 	// label update indirect, still same point, but point was updated
	l_hh.draw()
	l_ll.draw()
	l_p1.draw()
	l_p2.draw()
	
	// label update with new point
	l_p3.update(p3).draw()
	l_p4.update(p4).draw()

	// line update indirect
	lhh.draw(extend_only = false) // based on updated point instances hh and hhe
	lll.draw(extend_only = false) // based on updated point instances ll and lle

	// updating center label positions 
	var Label l_lhh = lhh.create_label('hh line\ncenter')
	l_lhh.update(lhh.create_center()).draw() // line center label needs update, since center is a separate point
	var CenterLabel l_lll = lll.create_center_label('ll line\ncenter', LabelArgs.new(style = label.style_label_up))
	l_lll.draw() // line center updated in draw()

	// test delayed init/plot
	l2 := na(l2) ? Line.new(p3, p2, LineArgs.new(color.fuchsia, width = 3)) : l2
	l2.draw(extend_only = false)
	var CenterLabel l_l2 = l2.create_center_label('🏂\n', LabelArgs.new(size = size.large, style = label.style_label_center))
	l_l2.draw()

	// test nested scope
	var chart.point fix3 = p3.copy()
	var chart.point fix4 = p4.copy()
	var Label l_fixp3 = fix3.create_label('fix 3').draw() // draw once
	var Label l_fixp4 = fix4.create_label('fix 4').draw() // draw once

	// extend only
	var chart.point p_dyn = chart.point.now(1)
	p_dyn.update()
	var Line l_dyn = Line.new(fix4, p_dyn)
	l_dyn.draw()
	var CenterLabel cl_dyn = l_dyn.create_center_label('cl_dyn')
	cl_dyn.draw()

	// line fill test between dynamic lines	
	var LineFill lf12 = lll.create_fill(l2,color.new(color.gray, 70))
	lf12.draw()

	var Box b1 = p4.create_box(p3, 'box', BoxArgs.new(bg_color = #00000000)) // expanding box, with one fix point p3 and one updating point p1
	b1.draw(extend_only = false)

	var Box b2 = p4.create_box(p3, 'boxt', BoxArgs.new(bg_color = #00000000, xloc = xloc.bar_time)) // expanding box, with one fix point p3 and one updating point p1
	if bar_index % 2 == 0
		b2.hide()
	b2.draw()

