--[[ Lua Print functions
/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2017 William Wilgus
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
]]

--[[ Exposed Functions

    _print.clear
    _print.f
    _print.opt
    _print.opt.area
    _print.opt.autoupdate
    _print.opt.color
    _print.opt.column
    _print.opt.defaults
    _print.opt.get
    _print.opt.justify
    _print.opt.line
    _print.opt.overflow
    _print.opt.sel_line
    _print.opt.set

]]

if not rb.lcd_framebuffer then rb.splash(rb.HZ, "No Support!") return nil end

local _print = {} do

    -- internal constants
    local _clr = require("color") -- _clr functions required

    local _NIL = nil -- _NIL placeholder
    local _LCD = rb.lcd_framebuffer()
    local WHITE = _clr.set(-1, 255, 255, 255)
    local BLACK = _clr.set(0, 0, 0, 0)
    local DRMODE_SOLID = 3
    local col_buf, s_lines = {}, {}
    local _p_opts = _NIL
    local tabstring = string.rep(" ", 2)
-- print internal helper functions
--------------------------------------------------------------------------------
   -- clamps value to >= min and <= max
    local function clamp(val, min, max)
        -- Warning doesn't check if min < max
        if val < min then
            return min
        elseif val < max then
            return val
        end
        return max
    end

    -- Gets size of text
    local function text_extent(msg, font)
        -- res, w, h
        return rb.font_getstringsize(msg, font or rb.FONT_UI)
    end

    -- Updates a single line on the screen
    local function update_line(enabled, opts, line, h)
        if enabled ~= true then return end
        local o = opts or _p_opts
    -- updates screen in specified rectangle
        rb.lcd_update_rect(o.x - 1, o.y + line * h,
             clamp(o.x + o.width, 1, rb.LCD_WIDTH) - 1,
             clamp(o.y + line * h + 1 + h, 1, rb.LCD_HEIGHT) - 1)
    end

    -- Clears a single line on the screen
    local function clear_line(opts, line, h)
        local o = opts or _p_opts
        _LCD:clear(o.bg_pattern, o.x, o.y + line * h + 1,
                                 o.x + o.width, line * h + h + o.y)
    end

    -- Sets the maximum number of lines on the screen
    local function max_lines(opts)
        local h  = opts.height
        local _, _, th = text_extent("W", opts.font)
        return h / th
    end

    --saves the items displayed for side to side scroll
    local function col_buf_insert(msg, line, _p_opts)
        --if _p_opts.line <= 1 then col_buf = {} end
        if not col_buf[line] then
            table.insert(col_buf, line, msg)
        end
    end

    --replaces / strips tab characters
    local function check_escapes(o, msg)
--[[    --for replacing a variety of escapes
        local tabsz  = 2
        local tabstr = string.rep(" ", tabsz)
        local function repl(esc)
            local ret = ""
            if esc:sub(1,1) == "\t" then ret = string.rep(tabstr, esc:len()) end
            return ret
        end
        msg = msg:gsub("(%c+)", repl)
]]
        msg = msg:gsub("\t", tabstring)
        local res, w, h = text_extent(msg, o.font)
        return w, h, msg
    end
--------------------------------------------------------------------------------
    local function set_linedesc(t_linedesc, opts)
        local o = opts or _print.opt.get(true)
        --local out = function() local t = {} for k, v in pairs(o) do t[#t + 1] = tostring(k) t[#t + 1] = tostring(v) end return table.concat(t, "\n") end
        --rb.splash_scroller(1000, out())
        local linedesc ={
                      --These are the defaults - changes will be made below if you supplied t_linedesc
                      indent = 0, -- internal indent text
                      line = 0, -- line index within group
                      nlines = 1, -- selection grouping
                      offset = 0, -- internal item offset
                      scroll = true,
                      selected = false, --internal
                      separator_height = 0,
                      line_separator = false,
                      show_cursor = false,
                      show_icons = false,
                      icon = -1,
                      icon_fn = function() return -1 end,
                      style = rb.STYLE_COLORBAR,
                      text_color = o.fg_pattern or WHITE,
                      line_color = o.bg_pattern or BLACK,
                      line_end_color= o.bg_pattern or BLACK,
                    }
        if type(t_linedesc) == "table" then

            if not o.linedesc then
                o.linedesc = {}
                for k, v in pairs(linedesc) do
                    o.linedesc[k] = v
                end
            end

            for k, v in pairs(t_linedesc) do
                o.linedesc[k] = v
            end
            if o.linedesc.separator_height > 0 then
                o.linedesc.line_separator = true
            end
            return
        end
        o.linedesc = linedesc
        return o.linedesc
    end

    -- set defaults for print view
    local function set_defaults()
        _p_opts = { x = 1,
                    y = 1,
                    width = rb.LCD_WIDTH - 1,
                    height = rb.LCD_HEIGHT - 1,
                    font = rb.FONT_UI,
                    drawmode = DRMODE_SOLID,
                    fg_pattern = WHITE,
                    bg_pattern = BLACK,
                    sel_pattern = WHITE,
                    line = 1,
                    max_line = _NIL,
                    col = 0,
                    header = false, --Internal use - treats first entry as header
                    ovfl = "auto", -- auto, manual, none
                    justify = "left", --left, center, right
                    autoupdate = true, --updates screen as items are added
                    drawsep = false,   -- separator between items
                  }
        set_linedesc(nil, _p_opts) -- default line display
        _p_opts.max_line = max_lines(_p_opts)

        s_lines, col_buf = {}, {}
        return _p_opts
    end

    -- returns table with settings for print
    -- if bByRef is _NIL or false then a copy is returned
    local function get_settings(bByRef)
        _p_opts = _p_opts or set_defaults()
        if not bByRef then
            -- shallow copy of table
            local copy = {}
            for k, v in pairs(_p_opts) do
                copy[k] = v
            end
            return copy
        end

        return _p_opts
    end

    -- sets the settings for print with your passed table
    local function set_settings(t_opts)
        _p_opts = t_opts or set_defaults()
        if t_opts then
            _p_opts.max_line = max_lines(_p_opts)
            col_buf = {}
        end
    end

    -- sets colors for print
    local function set_color(fgclr, bgclr, selclr)
        local o = get_settings(true)

        if fgclr ~= _NIL then
            o.fg_pattern, o.sel_pattern = fgclr, fgclr
        end
        o.sel_pattern = selclr or o.sel_pattern
        o.bg_pattern = bgclr or o.bg_pattern
    end

    -- helper function sets up colors/marker for selected items
    local function show_selected(iLine, msg)
        if rb.LCD_DEPTH  == 2 then -- invert 2-bit screens
            local o = get_settings() -- using a copy of opts so changes revert
            if not o then rb.set_viewport() return end
            o.fg_pattern = 3 - o.fg_pattern
            o.bg_pattern = 3 - o.bg_pattern
            rb.set_viewport(o)
            o = _NIL
        else
            show_selected = function() end -- no need to check again
        end
    end

    -- sets line explicitly or increments line if line is _NIL
    local function set_line(iLine)
        local o = get_settings(true)

        o.line = iLine or o.line + 1

        if(o.line < 1 or o.line > o.max_line) then
            o.line = 1
        end
    end

    -- clears the set print area
    local function clear()
        local o = get_settings(true)
        _LCD:clear(o.bg_pattern, o.x, o.y, o.x + o.width, o.y + o.height)
        if o.autoupdate == true then rb.lcd_update() end
        rb.lcd_scroll_stop()
        set_line(1)
        for i=1, #col_buf do col_buf[i] = _NIL end
        s_lines = {}
        collectgarbage("collect")
    end

    -- screen update after each call to print.f
    local function set_update(bAutoUpdate)
        local o = get_settings(true)
        o.autoupdate = bAutoUpdate or false
    end

    -- sets print area
    local function set_area(x, y, w, h)
        local o = get_settings(true)
        o.x, o.y = clamp(x, 1, rb.LCD_WIDTH), clamp(y, 1, rb.LCD_HEIGHT)
        o.width, o.height = clamp(w, 1, rb.LCD_WIDTH - o.x), clamp(h, 1, rb.LCD_HEIGHT - o.y)
        o.max_line = max_lines(_p_opts)

        clear()
        return o.line, o.max_line
    end

    -- when string is longer than print width scroll -- "auto", "manual", "none"
    local function set_overflow(str_mode)
        -- "auto", "manual", "none"
        local str_mode = str_mode or "auto"
        local o = get_settings(true)
        o.ovfl = str_mode:lower()
        col_buf = {}
    end

    -- aligns text to: "left", "center", "right"
    local function set_justify(str_mode)
        -- "left", "center", "right"
        local str_mode = str_mode or "left"
        local o = get_settings(true)
        o.justify = str_mode:lower()
    end

    -- selects line
    local function select_line(iLine)
        s_lines[iLine] = true
    end

   -- Internal print function
    local function print_internal(t_opts, x, w, h, msg)

        local linedesc
        local line_separator = false
        local o = t_opts
        local ld = o.linedesc or set_linedesc()
        local show_cursor = ld.show_cursor or 0
        local line_indent = 0

        local linestyle = ld.style or rb.STYLE_COLORBAR

        if o.justify ~= "left" then
            line_indent = (o.width - w) --"right"
            if o.justify == "center" then
                line_indent = line_indent / 2
            end
        end

        local line = o.line - 1 -- rb is 0-based lua is 1-based

        if o.ovfl == "manual" then --save msg for later side scroll
            col_buf_insert(msg, o.line, o)
        end

        -- bit of a pain to set the fields this way but its much more efficient than iterating a table to set them
        local function set_desc(tld, scroll, separator_height, selected, style, indent, text_color, line_color, line_end_color)
            tld.scroll = scroll
            tld.separator_height = separator_height
            tld.selected = selected
            tld.style = style
            tld.indent = indent
            tld.text_color = text_color
            tld.line_color = line_color
            tld.line_end_color = line_end_color
        end

        line_separator = ld.line_separator or o.drawsep
        local indent = line_indent < 0 and 0 or line_indent --rb scroller doesn't like negative offset!
        if o.line == 1 and o.header then
            set_desc(ld, true, 1, false, rb.STYLE_DEFAULT,
                     indent, o.fg_pattern, o.bg_pattern, o.bg_pattern)
            ld.show_cursor = false
        elseif s_lines[o.line] then
            --/* Display line selector */
            local style = show_cursor == true and rb.STYLE_DEFAULT or linestyle
            local ovfl = (o.ovfl == "auto" and w >= o.width and x == 0)
            set_desc(ld, ovfl, 0, true, style, indent,
                     o.bg_pattern, o.sel_pattern, o.sel_pattern)
        else
            set_desc(ld, false, 0, false, rb.STYLE_DEFAULT,line_indent,
                     o.fg_pattern, o.bg_pattern, o.bg_pattern)
        end

        if ld.show_icons then
            ld.icon = ld.icon_fn(line, ld.icon or -1)
        end

        rb.lcd_put_line(x, line *h, msg, ld)

        ld.show_cursor = show_cursor
        ld.style = linestyle
        if  line_separator then
            if ld.selected == true then
                rb.set_viewport(o) -- revert drawmode if selected
            end
            if not o.header then
                rb.lcd_drawline(0, line * h, o.width, line * h)
            end
            rb.lcd_drawline(0, line * h + h, o.width, line * h + h) --only to add the last line
            -- but we don't have an idea which line is the last line here so every line is the last line!
        end

        --only update the line we changed
        update_line(o.autoupdate, o, line, h)

        set_line(_NIL) -- increments line counter
    end

    -- Helper function that acts mostly like a normal printf() would
    local function printf(fmt, v1, ...)
        local o = get_settings(true)
        local w, h, msg, rep
        local line = o.line - 1 -- rb is 0-based lua is 1-based

        if not (fmt) or (fmt) == "\n" then -- handles blank line / single '\n'
             local res, w, h = text_extent(" ", o.font)

            clear_line(o, line, h)
            update_line(o.autoupdate, o, line, h)

            if (fmt) then set_line(_NIL) end

            return o.line, o.max_line, o.width, h
        end

        fmt, rep = fmt.gsub(fmt or "", "%%h", "%%s")
        o.header = (rep == 1)

        msg = string.format(fmt, v1, ...)

        show_selected(o.line, msg)

        w, h, msg = check_escapes(o, msg)

        print_internal(o, o.col, w, h, msg)

        return o.line, o.max_line, w, h
    end

    -- x > 0 scrolls right x < 0 scrolls left
    local function set_column(x)
        local o = get_settings()
        if o.ovfl ~= "manual" then return end -- no buffer stored to scroll
        rb.lcd_scroll_stop()

        local res, w, h, str, line

        for key, value in pairs(col_buf) do
            line = key - 1 -- rb is 0-based lua is 1-based
            o.line = key

            if value then
                show_selected(key, value)
                res, w, h = text_extent(value, o.font)
                clear_line(o, line, h)

                print_internal(o, x + o.col, w, h, value)
                update_line(o.autoupdate, o, line, h)
            end
        end
        o = _NIL
    end

    --expose functions to the outside through _print table
    _print.opt            = {}
    _print.opt.column     = set_column
    _print.opt.color      = set_color
    _print.opt.area       = set_area
    _print.opt.set        = set_settings
    _print.opt.get        = get_settings
    _print.opt.defaults   = set_defaults
    _print.opt.overflow   = set_overflow
    _print.opt.justify    = set_justify
    _print.opt.sel_line   = select_line
    _print.opt.line       = set_line
    _print.opt.linedesc   = set_linedesc
    _print.opt.autoupdate = set_update
    _print.selected       = function() return s_lines end
    _print.clear = clear
    _print.f     = printf

end --_print functions

return _print

