TableBuilder
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Electrified (electrifiedtrading)

// @version=5
// @description Utility for creating tables.
library('TableBuilder', true)
import Electrified/Color/8

//// Color Utility ///////////////////////////////
getFgColor(color fgColor, color bgColor) =>
    na(fgColor) ? Color.getContrastColor(bgColor) : fgColor

//////////////////////////////////////////////////
//// types ///////////////////////////////////////
//////////////////////////////////////////////////

// Represents the font properties of a text element in the table.
export type TextStyle
    color color // The color of the font.
    string size // The size of the font.
    string family // The font family.

// Represents the border or frame properties of a line element in the table.
export type LineStyle
    color color // The color of the line.
    int width // The width of the line.

// Represents the alignment properties of a cell in the table.
export type CellAlign
    string horizontal // The horizontal alignment of the cell contents.
    string vertical // The vertical alignment of the cell contents.

// Represents the style properties of a cell in the table.
export type CellStyle
    color bgColor // The background color of the cell.
    int width // The width of the cell.
    int height // The height of the cell.

// Represents text displayed by a cell.
export type Cell
    string contents // The cell contents.
    string tooltip // The cell tooltip.  

// Represents the style properties of the table.
export type TableStyle
    string position // The position of the table on the chart.
    color bgColor // The background color of the table.
    LineStyle border // The border style of the table.
    LineStyle frame // The frame style of the table.

// Represents the resultant size of a PineScript table.
export type TableSize
    int columns // The number of columns.
    int rows // The number of rows.

// Represents a row of data in the table.
export type Row
    Cell[] cells // An array of strings representing the contents of the cells in the row.
    int height // The desired height of the row.
    string tooltip // The default tooltip for the row.

// The result of initializing a table.
export type Table
    table table // The PineScript table created.
    TableSize size // The size of the table.

//////////////////////////////////////////////////
// @function Creates a new row with an empty array of cells.
// @param height The height of the row.
// @param tooltip The tooltip of the row.
// @returns A new Row object.
export createRow(int height = na, string tooltip = na) =>
    cells = array.new<Cell>()
    Row.new(cells, height, tooltip)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Creates a new row with an array of cells containing the given contents.
// @param contents An array of strings representing the cell contents.
// @param height The height of the row.
// @param tooltip The tooltip of the row.
// @returns A new Row object.
export createRow(string[] contents, int height = na, string tooltip = na) =>
    width = array.size(contents)
    cells = array.new<Cell>(width)
    for i = 0 to width==0 ? na : width - 1
        c = array.get(contents, i)
        array.set(cells, i, Cell.new(c, tooltip))
    Row.new(cells, height, tooltip)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Adds a new cell with the given contents to the array of cells.
// @param cells An array of cells.
// @param contents The contents of the new cell.
// @param tooltip The tooltip of the new cell.
// @returns The new Cell object.
export addCell(Cell[] cells, string contents, string tooltip = na) =>
    c = Cell.new(contents, tooltip)
    array.push(cells, c)
    c
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Adds a new cell with the given contents to the row.
// @param row The row to add the cell to.
// @param contents The contents of the new cell.
// @param tooltip The tooltip of the new cell.
// @returns The new Cell object.
export addCell(Row row, string contents, string tooltip = na) =>
    addCell(row.cells, contents, tooltip)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Adds a new cell with the given floating point number contents to the array of cells.
// @param cells An array of cells.
// @param contents The contents of the new cell, a floating point number.
// @param format The format string for the floating point number.
// @param tooltip The tooltip of the new cell.
// @returns The new Cell object.
export addCell(Cell[] cells, float contents, string format = na, string tooltip = na) =>
    c = Cell.new(na(contents) ? na : str.tostring(contents, format), tooltip)
    array.push(cells, c)
    c
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Adds a new cell with the given floating point number contents to the row.
// @param row The row to add the cell to.
// @param contents The contents of the new cell, a floating point number.
// @param format The format string for the floating point number.
// @param tooltip The tooltip of the new cell.
// @returns The new Cell object.
export addCell(Row row, float contents, string format = na, string tooltip = na) =>
    addCell(row.cells, contents, format, tooltip)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Adds a new cell with the given integer contents to the array of cells.
// @param cells An array of cells.
// @param contents The contents of the new cell, an integer.
// @param format The format string for the integer.
// @param tooltip The tooltip of the new cell.
// @returns The new Cell object.
export addCell(Cell[] cells, int contents, string format = na, string tooltip = na) =>
    c = Cell.new(na(contents) ? na : str.tostring(contents, format), tooltip)
    array.push(cells, c)
    c
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Adds a new cell with the given integer contents to the row.
// @param row The row to add the cell to.
// @param contents The contents of the new cell, an integer.
// @param format The format string for the integer.
// @param tooltip The tooltip of the new cell.
// @returns The new Cell object.
export addCell(Row row, int contents, string format = na, string tooltip = na) =>
    addCell(row.cells, contents, format, tooltip)

//////////////////////////////////////////////////
// @function Adds a new row to the array of rows using the given array of cells.
// @param rows An array of rows.
// @param cells The array of cells to use for the new row.
// @param height The height of the new row.
// @param tooltip The tooltip of the new row.
// @returns The new Row object.
export addRow(Row[] rows, Cell[] cells = na, int height = na, string tooltip = na) =>
    c = na(cells) ? array.new<Cell>() : cells
    r = Row.new(c, height, tooltip)
    array.push(rows, r)
    r
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Adds a new row to the array of rows using the given array of cell contents.
// @param rows An array of rows.
// @param contents The array of cell contents to use for the new row.
// @param height The height of the new row.
// @param tooltip The tooltip of the new row.
// @returns The new Row object.
export addRow(Row[] rows, string[] contents, int height = na, string tooltip = na) =>
    r = createRow(contents, height, tooltip)
    array.push(rows, r)
    r
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Updates a PineScript table with the style properties of the given TableStyle.
// @param t The PineScript table to update.
// @param style The TableStyle containing the style properties to apply.
export updateStyle(table t, TableStyle style) =>
    if not na(style)
        // Set the background color of the table.
        if not na(style.bgColor)
            table.set_bgcolor(t, style.bgColor)

        // Set the border properties of the table.
        if not na(style.border)
            if not na(style.border.color)
                table.set_border_color(t, style.border.color)
            if not na(style.border.width)
                table.set_border_width(t, style.border.width)

        // Set the frame properties of the table.
        if not na(style.frame)
            if not na(style.frame.color)
                table.set_frame_color(t, style.frame.color)
            if not na(style.frame.width)
                table.set_frame_width(t, style.frame.width)
                
        // Set the position of the table.
        if not na(style.position)
            table.set_position(t, style.position)
//////////////////////////////////////////////////


//////////////////////////////////////////////////
// @function Updates a row of cells in a table with the given cell content.
// @param t The table to update.
// @param row The index of the row to update.
// @param cells The array of cells to set in the row.
// @param tooltip The optional default tooltip to use if the cell's value is na.
export updateRow(table t, int row, Cell[] cells, string tooltip = na) =>
    cellCount = array.size(cells)
    for c = 0 to cellCount==0 ? na : cellCount - 1
        cell = array.get(cells, c)
        if not na(cell)
            if not na(cell.contents)
                table.cell_set_text(t, c, row, cell.contents)
            tt = na(cell.tooltip) ? tooltip : cell.tooltip
            if not na(tt)
                table.cell_set_tooltip(t, c, row, tt)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Updates a row of cells in a table with the given cell content.
// @param t The table to update.
// @param row The index of the row to update.
// @param cells The array of cell content to set in the row.
// @param tooltip The optional default tooltip to use if the cell's value is na.
export updateRow(table t, int row, string[] cells, string tooltip = na) =>
    cellCount = array.size(cells)
    for c = 0 to cellCount==0 ? na : cellCount - 1
        cell = array.get(cells, c)
        if not na(cell)
            table.cell_set_text(t, c, row, cell)
        if not na(tooltip)
            table.cell_set_tooltip(t, c, row, tooltip)
//////////////////////////////////////////////////            

//////////////////////////////////////////////////
// @function Initializes a new PineScript table with the given data rows and table style.
// @param rows An array of DataRows to populate the table with.
// @param style The TableStyle to apply to the table.
// @returns The new PineScript table.
export initialize(Row[] rows, TableStyle style) =>
    // Calculate the number of rows and columns in the table
    numRows = array.size(rows)
    numColumns = 0
    for row in rows
        numColumns := math.max(numColumns, array.size(row.cells))

    // Create the table
    t = table.new(style.position, math.max(numColumns, 1), math.max(numRows, 1))

    // Apply the table style
    updateStyle(t, style)

    color fgColor = na(style.bgColor) ? color.gray : Color.getContrastColor(style.bgColor)
    
    // Iterate through the rows and cells and set their styles
    for r = 0 to numRows == 0 ? na : numRows - 1
        row = array.get(rows, r)
        cells = row.cells
        cellCount = array.size(cells)
        for c = 0 to cellCount == 0 ? na : cellCount - 1
            cell = array.get(cells, c)

            // Set the cell text and tooltips
            if not na(cell.contents)
                table.cell_set_text(t, c, r, cell.contents)
            
            tooltip = na(cell.tooltip) ? row.tooltip : cell.tooltip
            if not na(tooltip)
                table.cell_set_tooltip(t, c, r, tooltip)

            // Set the cell text color based on the table background color
            table.cell_set_text_color(t, c, r, fgColor)

    Table.new(t, TableSize.new(numColumns, numRows))
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Initializes a new PineScript table with the given data rows and table style.
// @param rows An array of Rows to populate the table with.
// @param position The position of the table within the chart.
// @param bgColor The optional background color for the table.
// @param border The optional border style for the table.
// @param frame The optional frame style for the table.
// @returns The new PineScript table.
export initialize(Row[] rows, string position, color bgColor, LineStyle border = na, LineStyle frame = na) =>
    initialize(rows, TableStyle.new(position, bgColor, border, frame))
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Initializes a new PineScript table with the given data rows and table style.
// @param rows An array of Rows to populate the table with.
// @param position The position of the table within the chart.
// @param border The optional border style for the table.
// @param frame The optional frame style for the table.
// @returns The new PineScript table.
export initialize(Row[] rows, string position, LineStyle border = na, LineStyle frame = na) =>
    initialize(rows, TableStyle.new(position, na, border, frame))
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given range of the table with the given cell and text styles, and cell alignment.
// @param t The table to style.
// @param firstCol The index of the first column in the range.
// @param lastCol The index of the last column in the range.
// @param firstRow The index of the first row in the range.
// @param lastRow The index of the last row in the range.
// @param cellStyle The cell style to apply.
// @param textStyle The text style to apply.
// @param align The cell alignment to apply.
export style(table t, int firstCol, int lastCol, int firstRow, int lastRow, CellStyle cellStyle = na, TextStyle textStyle = na, CellAlign align = na) =>
    // capture the colors
    color bgColor = na(cellStyle) ? na : (na(cellStyle.bgColor) ? na : cellStyle.bgColor)
    color fgColor = getFgColor(na(textStyle) ? na : (na(textStyle.color) ? na : textStyle.color), bgColor)

    if firstCol >= 0 and lastCol >= 0 and firstRow >= 0 and lastRow >= 0

        // set all the values
        for r = firstRow to lastRow
            for c = firstCol to lastCol
                if not na(bgColor)
                    table.cell_set_bgcolor(t, c, r, bgColor)
                if not na(fgColor)
                    table.cell_set_text_color(t, c, r, fgColor)
                if not na(textStyle)
                    if not na(textStyle.size)
                        table.cell_set_text_size(t, c, r, textStyle.size)
                    if not na(textStyle.family)
                        table.cell_set_text_font_family(t, c, r, textStyle.family)
                if not na(align)
                    if not na(align.horizontal)
                        table.cell_set_text_halign(t, c, r, align.horizontal)
                    if not na(align.vertical)
                        table.cell_set_text_valign(t, c, r, align.vertical)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles all the cells in the table.
// @param t The Table to style.
// @param cellStyle The cell style to apply.
// @param textStyle The text style to apply.
// @param align The cell alignment to apply.
export style(Table t, CellStyle cellStyle = na, TextStyle textStyle = na, CellAlign align = na) =>
    style(t.table, 0, t.size.columns-1, 0, t.size.rows-1, cellStyle, textStyle, align)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given range of the table with the given background color and foreground color.
// @param t The table to style.
// @param firstCol The index of the first column in the range.
// @param lastCol The index of the last column in the range.
// @param firstRow The index of the first row in the range.
// @param lastRow The index of the last row in the range.
// @param bgColor The background color to apply.
export style(table t, int firstCol, int lastCol, int firstRow, int lastRow, color bgColor, color fgColor) =>
    fgc = getFgColor(fgColor, bgColor)
    for r = firstRow to lastRow
        for c = firstCol to lastCol
            if not na(bgColor)
                table.cell_set_bgcolor(t, c, r, bgColor)
            if not na(fgc)
                table.cell_set_text_color(t, c, r, fgc)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given range of the table with the given background color while automatically chosing a foreground color.
// @param t The table to style.
// @param firstCol The index of the first column in the range.
// @param lastCol The index of the last column in the range.
// @param firstRow The index of the first row in the range.
// @param lastRow The index of the last row in the range.
// @param bgColor The background color to apply.
export style(table t, int firstCol, int lastCol, int firstRow, int lastRow, color bgColor) =>
    style(t, firstCol, lastCol, firstRow, lastRow, bgColor, na)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given range of columns of the table with the given cell and text styles, and cell alignment.
// @param t The table to style.
// @param first The index of the first column in the range.
// @param last The index of the last column in the range.
// @param cellStyle The cell style to apply.
// @param textStyle The text style to apply.
// @param align The cell alignment to apply.
export styleColumns(Table t, int first, int last, CellStyle cellStyle = na, TextStyle textStyle = na, CellAlign align = na) =>
    style(t.table, first, last, 0, t.size.rows-1, cellStyle, textStyle, align)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given column of the table with the given cell and text styles, and cell alignment.
// @param t The table to style.
// @param col The index of the column.
// @param cellStyle The cell style to apply.
// @param textStyle The text style to apply.
// @param align The cell alignment to apply.
export styleColumn(Table t, int col, CellStyle cellStyle = na, TextStyle textStyle = na, CellAlign align = na) =>
    styleColumns(t, col, col, cellStyle, textStyle, align)
//////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
// @function Styles the cells in the given range of rows of the table with the given cell and text styles, and cell alignment.
// @param t The table to style.
// @param first The index of the first row in the range.
// @param last The index of the last row in the range.
// @param cellStyle The cell style to apply.
// @param textStyle The text style to apply.
// @param align The cell alignment to apply.
export styleRows(Table t, int first, int last, CellStyle cellStyle = na, TextStyle textStyle = na, CellAlign align = na) =>
    style(t.table, 0, t.size.columns-1, first, last, cellStyle, textStyle, align)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given row of the table with the given cell and text styles, and cell alignment
// @param t The table to style.
// @param col The index of the row.
// @param cellStyle The cell style to apply.
// @param textStyle The text style to apply.
// @param align The cell alignment to apply.
export styleRow(Table t, int col, CellStyle cellStyle = na, TextStyle textStyle = na, CellAlign align = na) =>
    styleRows(t, col, col, cellStyle, textStyle, align)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given range of columns of the table with the given background color and foreground color.
// @param t The table to style.
// @param first The index of the first column in the range.
// @param last The index of the last column in the range.
// @param bgColor The background color to apply.
// @param fgColor The foreground color to apply.
export styleColumns(Table t, int first, int last, color bgColor, color fgColor) =>
    style(t.table, first, last, 0, t.size.rows-1, bgColor, fgColor)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given column of the table with the given background color and foreground color.
// @param t The table to style.
// @param col The index of the column.
// @param bgColor The background color to apply.
// @param fgColor The foreground color to apply.
export styleColumn(Table t, int col, color bgColor, color fgColor) =>
    styleColumns(t, col, col, bgColor, fgColor)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given range of rows of the table with the given background color and foreground color.
// @param t The table to style.
// @param first The index of the first row in the range.
// @param last The index of the last row in the range.
// @param bgColor The background color to apply.
// @param fgColor The foreground color to apply.
export styleRows(Table t, int first, int last, color bgColor, color fgColor) =>
    style(t.table, 0, t.size.columns-1, first, last, bgColor, fgColor)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given row of the table with the given background color and foreground color.
// @param t The table to style.
// @param col The index of the row.
// @param bgColor The background color to apply.
// @param fgColor The foreground color to apply.
export styleRow(Table t, int col, color bgColor, color fgColor) =>
    styleRows(t, col, col, bgColor, fgColor)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given range of columns of the table with the given background color while automatically chosing a foreground color.
// @param t The table to style.
// @param first The index of the first column in the range.
// @param last The index of the last column in the range.
// @param bgColor The background color to apply.
export styleColumns(Table t, int first, int last, color bgColor) =>
    style(t.table, first, last, 0, t.size.rows-1, bgColor, na)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given column of the table with the given background color while automatically chosing a foreground color.
// @param t The table to style.
// @param col The index of the column.
// @param bgColor The background color to apply.
export styleColumn(Table t, int col, color bgColor) =>
    styleColumns(t, col, col, bgColor)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given range of rows of the table with the given background color while automatically chosing a foreground color.
// @param t The table to style.
// @param first The index of the first row in the range.
// @param last The index of the last row in the range.
// @param bgColor The background color to apply.
export styleRows(Table t, int first, int last, color bgColor) =>
    style(t.table, 0, t.size.columns-1, first, last, bgColor, na)
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given row of the table with the given background color while automatically chosing a foreground color.
// @param t The table to style.
// @param col The index of the row.
// @param bgColor The background color to apply.
export styleRow(Table t, int col, color bgColor) =>
    styleRows(t, col, col, bgColor)    
//////////////////////////////////////////////////

//////////////////////////////////////////////////
// @function Styles the cells in the given range of rows of the table with the given background color while automatically chosing a foreground color.
// @param t The table to style.
// @param first The index of the first row in the range.
// @param last The index of the last row in the range.
// @param bgColor The background color to apply.
export styleRows(Table t, color[] rowColors) =>
    last = array.size(rowColors) - 1
    for row = 0 to last==-1 ? na : last
        c = array.get(rowColors, row)
        styleRow(t, row, c)
//////////////////////////////////////////////////


//////////////////////////////////////////////////
//// Demo ////////////////////////////////////////
//////////////////////////////////////////////////

// Setup the rows.
rows = array.new<Row>()

// Add the header
header = addRow(rows)
addCell(header, "Index")
addCell(header, "Value")
addCell(header, "Percent")

// First create the data.
for i = 1 to 10
    row = addRow(rows, tooltip = "Row " + str.tostring(i))
    addCell(row, i)
    addCell(row, i*2000)
    addCell(row, i*10, format.percent)

tbl = initialize(rows, position.top_right,
  frame = LineStyle.new(color.white, 1))

// Align the cells to the right.
style(tbl, align = CellAlign.new(text.align_right))

// Style the columnS.
styleColumn(tbl, 1, color.blue)
styleColumn(tbl, 2, #0022AA, #FF22AA)

// Style the header row.
styleRow(tbl, 0, color.white)
 