lib_log
// 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 for logging and debugging pine scripts
library("lib_log", overlay = true)

///////////////////////////////////////////
//               HELPERS
///////////////////////////////////////////
level_color(int level, int transparency = 0) =>
    col = switch level
        4 => color.red
        3 => color.orange
        2 => color.green
        1 => color.aqua
        => chart.bg_color
    color.new(col, transparency)

get_level_name(int level) =>
    switch level
        4 => 'ERROR'
        3 => 'WARNING'
        2 => 'SUCCESS'
        1 => 'INFO'
        => 'DEBUG'


///////////////////////////////////////////
//               MODEL
///////////////////////////////////////////


export type LogEntry
    int timestamp
    int bar
    int level
    string logger
    string message


export type LogDisplay
    int max_lines = 5
    string table_pos = position.bottom_left
    table _table = na
    array<LogEntry> log = na
    

export type Logger
    string name = na
    // 3 : ERROR
    // 2 : WARNING
    // 1 : INFO
    // 0 : DEBUG
    int min_level = 0

    // table
    bool color_logs = false
    LogDisplay display = na

    


///////////////////////////////////////////
//               PRIVATE
///////////////////////////////////////////


method init(LogDisplay this) =>
    if na(this.log)
        this.log := array.new<LogEntry>(this.max_lines)
    if na(this._table) and not na(this.log)
        this._table := table.new(this.table_pos, 5, 1 + this.max_lines, chart.bg_color, border_color = chart.bg_color, border_width = 1)
        this._table.cell(0, 0, 'time', text_color = chart.fg_color, text_halign = text.align_center, text_size = size.small, bgcolor = chart.bg_color)
        this._table.cell(1, 0, 'bar', text_color = chart.fg_color, text_halign = text.align_center, text_size = size.small, bgcolor = chart.bg_color)
        this._table.cell(2, 0, 'level', text_color = chart.fg_color, text_halign = text.align_center, text_size = size.small, bgcolor = chart.bg_color)
        this._table.cell(3, 0, na, text_color = chart.fg_color, text_halign = text.align_center, text_size = size.small, bgcolor = chart.bg_color)
        this._table.cell(4, 0, 'log', text_color = chart.fg_color, text_halign = text.align_center, text_size = size.small, bgcolor = chart.bg_color)
        for i = 1 to this.max_lines
            this._table.cell(0, i, na, text_color = chart.fg_color, text_halign = text.align_right, text_size = size.small, bgcolor = chart.bg_color)
            this._table.cell(1, i, na, text_color = chart.fg_color, text_halign = text.align_right, text_size = size.small, bgcolor = chart.bg_color)
            this._table.cell(2, i, na, text_color = chart.fg_color, text_halign = text.align_right, text_size = size.small, bgcolor = chart.bg_color)
            this._table.cell(3, i, na, text_color = chart.fg_color, text_halign = text.align_left, text_size = size.small, bgcolor = chart.bg_color)
            this._table.cell(4, i, na, text_color = chart.fg_color, text_halign = text.align_left, text_size = size.small, bgcolor = chart.bg_color)
    this


method initDisplay(Logger this) =>
    if na(this.display)
        this.display := LogDisplay.new()
    this.display.init()
    this

method get_level_color(Logger this, int level, int transparency = 0) =>
    level_color(this.color_logs ? level : 0, transparency)

method log_bar(Logger this, int level, string message, int bar = bar_index, float y = high, float y_offset = 0, bool last_only = false, bool condition = true) =>
    var last = label.new(na, na, na, size = size.small, textcolor = chart.fg_color)
    if condition and level >= this.min_level
        lbl = last_only ? last : label.new(na, na, na, size = size.small, textcolor = chart.fg_color)
        lbl.set_text(message)
        lbl.set_xy(bar, y + y_offset)
        lbl.set_style(y_offset >= 0 ? label.style_label_down: label.style_label_up)
        lbl.set_color(this.get_level_color(level, 80))


method recycle(LogEntry this, int level, string message, string logger) =>
    le = this
    if na(le)
        le := LogEntry.new(time, bar_index, level, logger, message)
    else
        le.timestamp := time
        le.bar := bar_index
        le.level := level
        le.logger := logger
        le.message := message
    le


method log_table(Logger this, int level, string message, bool condition = true) =>
    this.initDisplay()

    if condition and level >= this.min_level
        temp = this.display.log.shift().recycle(level, message, this.name)
        this.display.log.push(temp)

        for [i,le] in this.display.log
            if not na(le)
                cell_color = this.get_level_color(le.level, 80)
                this.display._table.cell_set_text(0, i+1, str.format('{0,date,y.MM.dd hh:mm}', le.timestamp))
                this.display._table.cell_set_text(1, i+1, str.tostring(le.bar))
                this.display._table.cell_set_text(2, i+1, get_level_name(le.level))
                this.display._table.cell_set_text(3, i+1, le.logger)
                this.display._table.cell_set_text(4, i+1, le.message)
                this.display._table.cell_set_bgcolor(0, i+1, cell_color)
                this.display._table.cell_set_bgcolor(1, i+1, cell_color)
                this.display._table.cell_set_bgcolor(2, i+1, cell_color)
                this.display._table.cell_set_bgcolor(3, i+1, cell_color)
                this.display._table.cell_set_bgcolor(4, i+1, cell_color)


///////////////////////////////////////////
//               PUBLIC
///////////////////////////////////////////

//@description add a log entry to the log table
//@param this Logger to add the entry to
//@param message The Message to add
//@param condition optional flag to enable disable logging of this entry dynamically (default: true)
export method debug(Logger this, string message, bool condition = true) => this.log_table(0, message, condition)

//@description add a log entry to the log table
//@param this Logger to add the entry to
//@param message The Message to add
//@param condition optional flag to enable disable logging of this entry dynamically (default: true)
export method info(Logger this, string message, bool condition = true) => this.log_table(1, message, condition)
    
//@description add a log entry to the log table
//@param this Logger to add the entry to
//@param message The Message to add
//@param condition optional flag to enable disable logging of this entry dynamically (default: true)
export method success(Logger this, string message, bool condition = true) => this.log_table(2, message, condition)
    
//@description add a log entry to the log table
//@param this Logger to add the entry to
//@param message The Message to add
//@param condition optional flag to enable disable logging of this entry dynamically (default: true)
export method warning(Logger this, string message, bool condition = true) => this.log_table(3, message, condition)
    
//@description add a log entry to the log table
//@param this Logger to add the entry to
//@param message The Message to add
//@param condition optional flag to enable disable logging of this entry dynamically (default: true)
export method error(Logger this, string message, bool condition = true) => this.log_table(4, message, condition)



//@description for debug label on a bar
//@param this Logger object to check global min level condition
//@param message The string to print
//@param bar The bar to print the label at (default: bar_index)
//@param y The price value to print at (default: high)
//@param y_offset A price offset from y if you want to print multiple labels at the same spot
export method debug_bar(Logger this, string message, int bar = bar_index, float y = high, float y_offset = 0, bool last_only = false, bool condition = true) =>
    this.log_bar(0, message, bar, y, y_offset, last_only, condition)

//@description for debug label on a bar
//@param this Logger object to check global min level condition
//@param message The string to print
//@param bar The bar to print the label at (default: bar_index)
//@param y The price value to print at (default: high)
//@param y_offset A price offset from y if you want to print multiple labels at the same spot
export method info_bar(Logger this, string message, int bar = bar_index, float y = high, float y_offset = 0, bool last_only = false, bool condition = true) =>
    this.log_bar(1, message, bar, y, y_offset, last_only, condition)

//@description for debug label on a bar
//@param this Logger object to check global min level condition
//@param message The string to print
//@param bar The bar to print the label at (default: bar_index)
//@param y The price value to print at (default: high)
//@param y_offset A price offset from y if you want to print multiple labels at the same spot
export method success_bar(Logger this, string message, int bar = bar_index, float y = high, float y_offset = 0, bool last_only = false, bool condition = true) =>
    this.log_bar(2, message, bar, y, y_offset, last_only, condition)

//@description for debug label on a bar
//@param this Logger object to check global min level condition
//@param message The string to print
//@param bar The bar to print the label at (default: bar_index)
//@param y The price value to print at (default: high)
//@param y_offset A price offset from y if you want to print multiple labels at the same spot
export method warning_bar(Logger this, string message, int bar = bar_index, float y = high, float y_offset = 0, bool last_only = false, bool condition = true) =>
    this.log_bar(3, message, bar, y, y_offset, last_only, condition)

//@description for debug label on a bar
//@param this Logger object to check global min level condition
//@param message The string to print
//@param bar The bar to print the label at (default: bar_index)
//@param y The price value to print at (default: high)
//@param y_offset A price offset from y if you want to print multiple labels at the same spot
export method error_bar(Logger this, string message, int bar = bar_index, float y = high, float y_offset = 0, bool last_only = false, bool condition = true) =>
    this.log_bar(4, message, bar, y, y_offset, last_only, condition)
            


///////////////////////////////////////////
//                  USAGE
///////////////////////////////////////////

// configure the logger to plot Debug or above, max 10 entries in the table and to enable colors
var LogDisplay d = LogDisplay.new(10, position.bottom_right).init()
var Logger l1 = Logger.new('L1', min_level = 0, color_logs = true, display = d)
var Logger l2 = Logger.new('L2', min_level = 0, color_logs = true, display = d)
var Logger l3 = Logger.new('L3', min_level = 2)

// realtime data
varip tick = 0
if barstate.isnew
    tick := 0
tick += 1

// display auto init
if barstate.isfirst
    l3.debug('debug ' + str.tostring(tick))
    l3.info('info ' + str.tostring(tick))
    l3.success('success ' + str.tostring(tick))
    l3.warning('warning ' + str.tostring(tick))
l3.error('error ' + str.tostring(tick))

// helper for stacking labels (only necessary if you want to plot multiple labels per bar)
atr = ta.atr(100)

// simple label above bar
l1.debug_bar('debug')
l1.info_bar('info', y_offset = atr)
l1.success_bar('success', y_offset = atr*2)
l1.warning_bar('warning', y_offset = atr*3)
l1.error_bar('error', y_offset = atr*4)

// trailing label below last bar (negative offset switches label to point upwards)
if barstate.islast
    l2.debug_bar('last debug ' + str.tostring(tick), y_offset = -atr, last_only = true)
    l2.info_bar('last info ' + str.tostring(tick), y_offset = -atr*2, last_only = true)
    l2.success_bar('last success ' + str.tostring(tick), y_offset = -atr*3, last_only = true)
    l2.warning_bar('last warning ' + str.tostring(tick), y_offset = -atr*4, last_only = true)
    l2.error_bar('last error ' + str.tostring(tick), y_offset = -atr*5, last_only = true)

// entries into the log table
l1.debug('debug ' + str.tostring(tick))
l2.info('info ' + str.tostring(tick))
l1.success('success ' + str.tostring(tick))
l2.warning('warning ' + str.tostring(tick))
l1.error('error ' + str.tostring(tick))
