ftext.lua
local ftext = {}
local dec = {"d", "decimal", "dec"}
local hex = {"h", "hexidecimal", "hex"}
local col = {"c", "color", "colour", "col", "name"}
function ftext.wordWrap(str, limit, indent, indent1)
    indent = indent or ""
  indent1 = indent1 or indent
  limit = limit or 72
  local here = 1 - #indent1
  local function check(sp, st, word, fi)
    if fi - here > limit then
      here = st - #indent
      return "\n" .. indent .. word
    end
  end
  return indent1 .. str:gsub("(%s+)()(%S+)()", check)
end
function ftext.xwrap(text, limit, type)
  local colorPattern
  if table.contains(dec, type) then
    colorPattern = _Echos.Patterns.Decimal[1]
  elseif table.contains(hex, type) then
    colorPattern = _Echos.Patterns.Hex[1]
  elseif table.contains(col, type) then
    colorPattern = _Echos.Patterns.Color[1]
  else
    return ftext.wordWrap(text, limit)
  end
  local strippedString = rex.gsub(text, colorPattern, "")
  local strippedLines = ftext.wordWrap(strippedString, limit):split("\n")
  local lineIndex = 1
  local line = ""
  local strLine = ""
  local lines = {}
  local strLines = {}
  local workingLine = strippedLines[lineIndex]:split("")
  local workingLineLength = #workingLine
  local lineColumn = 0
  for str, color, res in rex.split(text, colorPattern) do
    if res then
      if type == "Hex" then
        color = "#r"
      elseif type == "Dec" then
        color = "<r>"
      elseif type == "Color" then
        color = "<reset>"
      end
    end
    color = color or ""
    local strLen = str:len()
    if lineColumn + strLen <= workingLineLength then
      strLine = strLine .. str
      line = line .. str .. color
      lineColumn = lineColumn + strLen
    else
      local neededChars = workingLineLength - lineColumn
      local take = str:sub(1, neededChars)
      local leave = str:sub(neededChars + 1, -1)
      strLine = strLine .. take
      line = line .. take
      table.insert(lines, line)
      table.insert(strLines, strLine)
      line = ""
      strLine = ""
      lineIndex = lineIndex + 1
      workingLine = strippedLines[lineIndex]:split("")
      workingLineLength = #workingLine
      lineColumn = 0
      if leave:sub(1, 1) == " " then
        leave = leave:sub(2, -1)
      end
      while leave ~= "" do
        take = leave:sub(1, workingLineLength)
        leave = leave:sub(workingLineLength + 1, -1)
        if leave:sub(1, 1) == " " then
          leave = leave:sub(2, -1)
        end
        if take:len() < workingLineLength then
          lineColumn = take:len()
          line = line .. take .. color
          strLine = strLine .. take
        else
          lineIndex = lineIndex + 1
          workingLine = strippedLines[lineIndex]
          if workingLine then
            workingLine = strippedLines[lineIndex]:split("")
            workingLineLength = #workingLine
          end
          table.insert(lines, take)
          table.insert(strLines, take)
        end
        if leave == "\n" then
          table.insert(lines, leave)
          table.insert(strLines, leave)
          leave = ""
        end
      end
    end
  end
  if line ~= "" then
    table.insert(lines, line)
  end
  return table.concat(lines, "\n")
end
function ftext.fText(str, opts)
  local options = ftext.fixFormatOptions(str, opts)
  if options.wrap and (options.strLen > options.effWidth) then
    local wrapped = ""
    if str:find("\n") then
      for _,line in ipairs(str:split("\n")) do
        local newline = "\n"
        if _ == 1 then newline = "" end
        wrapped = wrapped .. newline .. ftext.xwrap(line, options.effWidth, options.formatType)
      end
    else
      wrapped = ftext.xwrap(str, options.effWidth, options.formatType)
    end
    local lines = wrapped:split("\n")
    local formatted = {}
    options.fixed = false
    for _, line in ipairs(lines) do
      table.insert(formatted, ftext.fLine(line, options))
    end
    return table.concat(formatted, "\n")
  else
    return ftext.fLine(str, options)
  end
end
function ftext.fixFormatOptions(str, opts)
  if opts.fixed then
    return table.deepcopy(opts)
  end
    if opts == nil then
    opts = {}
  end     if type(opts) ~= "table" then
    error("Improper argument: options expected to be passed as table")
  end
    local options = table.deepcopy(opts)
  if options.wrap == nil then
    options.wrap = true
  end   if options.truncate == nil then
    options.truncate = false
  end   options.formatType = options.formatType or ""   options.width = options.width or 80   options.cap = options.cap or ""   options.spacer = options.spacer or " "   options.alignment = options.alignment or "center"   if options.nogap == nil then
    options.nogap = false
  end
  if options.inside == nil then
    options.inside = false
  end   if not options.mirror == false then
    options.mirror = options.mirror or true
  end     if table.contains(dec, options.formatType) then
    options.capColor = options.capColor or "<255,255,255>"
    options.spacerColor = options.spacerColor or "<255,255,255>"
    options.textColor = options.textColor or "<255,255,255>"
    options.colorReset = "<r>"
    options.colorPattern = _Echos.Patterns.Decimal[1]
  elseif table.contains(hex, options.formatType) then
    options.capColor = options.capColor or "#FFFFFF"
    options.spacerColor = options.spacerColor or "#FFFFFF"
    options.textColor = options.textColor or "#FFFFFF"
    options.colorReset = "#r"
    options.colorPattern = _Echos.Patterns.Hex[1]
  elseif table.contains(col, options.formatType) then
    options.capColor = options.capColor or "<white>"
    options.spacerColor = options.spacerColor or "<white>"
    options.textColor = options.textColor or "<white>"
    options.colorReset = "<reset>"
    options.colorPattern = _Echos.Patterns.Color[1]
  else
    options.capColor = ""
    options.spacerColor = ""
    options.textColor = ""
    options.colorReset = ""
    options.colorPattern = ""
  end
  options.originalString = str
  options.strippedString = rex.gsub(tostring(str), options.colorPattern, "")
  options.strLen = string.len(options.strippedString)
  options.leftCap = options.cap
  options.rightCap = options.cap
  options.capLen = string.len(options.cap)
  local gapSpaces = 0
  if not options.nogap then
    if options.alignment == "center" then
      gapSpaces = 2
    else
      gapSpaces = 1
    end
  end
  options.nontextlength = options.width - options.strLen - gapSpaces
  options.leftPadLen = math.floor(options.nontextlength / 2)
  options.rightPadLen = options.nontextlength - options.leftPadLen
  options.effWidth = options.width - ((options.capLen * gapSpaces) + gapSpaces)
  if options.capLen > options.leftPadLen then
    options.cap = options.cap:sub(1, options.leftPadLen)
    options.capLen = string.len(options.cap)
  end
  options.fixed = true
  return options
end
function ftext.fLine(str, opts)
  local options = ftext.fixFormatOptions(str, opts)
  local truncate, strLen, width = options.truncate, options.strLen, options.width
  if truncate and strLen > width then
    local wrapped = ftext.xwrap(str, options.effWidth, options.formatType)
    local lines = wrapped:split("\n")
    str = lines[1]
  end
  local leftCap = options.leftCap
  local rightCap = options.rightCap
  local leftPadLen = options.leftPadLen
  local rightPadLen = options.rightPadLen
  local capLen = options.capLen
  if options.alignment == "center" then     if options.mirror then       rightCap = string.gsub(rightCap, "<", ">")
      rightCap = string.gsub(rightCap, "%[", "%]")
      rightCap = string.gsub(rightCap, "{", "}")
      rightCap = string.gsub(rightCap, "%(", "%)")
      rightCap = string.reverse(rightCap)
    end     if not options.nogap then
      str = string.format(" %s ", str)
    end
  elseif options.alignment == "right" then     leftPadLen = leftPadLen + rightPadLen
    rightPadLen = 0
    rightCap = ""
    if not options.nogap then
      str = string.format(" %s", str)
    end
  else     rightPadLen = rightPadLen + leftPadLen
    leftPadLen = 0
    leftCap = ""
    if not options.nogap then
      str = string.format("%s ", str)
    end
  end   local fullLeftCap = string.format("%s%s%s", options.capColor, leftCap, options.colorReset)
  local fullLeftSpacer = string.format("%s%s%s", options.spacerColor, string.rep(options.spacer, (leftPadLen - capLen)), options.colorReset)
  local fullText = string.format("%s%s%s", options.textColor, str, options.colorReset)
  local fullRightSpacer = string.format("%s%s%s", options.spacerColor, string.rep(options.spacer, (rightPadLen - capLen)), options.colorReset)
  local fullRightCap = string.format("%s%s%s", options.capColor, rightCap, options.colorReset)
  if options.inside then
                    local finalString = string.format("%s%s%s%s%s", fullLeftCap, fullLeftSpacer, fullText, fullRightSpacer, fullRightCap)
    return finalString
  else
                
    local finalString = string.format("%s%s%s%s%s", fullLeftSpacer, fullLeftCap, fullText, fullRightCap, fullRightSpacer)
    return finalString
  end
end
function ftext.align(str, opts)
  local options = {}
  if opts == nil then
    opts = {}
  end
  if type(opts) == "table" then
    options = table.deepcopy(opts)
    options.formatType = ""
    options.wrap = false
  else
    error("Improper argument: options expected to be passed as table")
  end
  options = ftext.fixFormatOptions(str, options)
  return ftext.fLine(str, options)
end
function ftext.dalign(str, opts)
  local options = {}
  if opts == nil then
    opts = {}
  end
  if type(opts) == "table" then
    options = table.deepcopy(opts)
    options.formatType = "d"
    options.wrap = false
  else
    error("Improper argument: options expected to be passed as table")
  end
  options = ftext.fixFormatOptions(str, options)
  return ftext.fLine(str, options)
end
function ftext.calign(str, opts)
  local options = {}
  if opts == nil then
    opts = {}
  end
  if type(opts) == "table" then
    options = table.deepcopy(opts)
    options.formatType = "c"
    options.wrap = false
  else
    error("Improper argument: options expected to be passed as table")
  end
  options = ftext.fixFormatOptions(str, options)
  return ftext.fLine(str, options)
end
function ftext.halign(str, opts)
  local options = {}
  if opts == nil then
    opts = {}
  end
  if type(opts) == "table" then
    options = table.deepcopy(opts)
    options.formatType = "h"
    options.wrap = false
  else
    error("Improper argument: options expected to be passed as table")
  end
  options = ftext.fixFormatOptions(str, options)
  return ftext.fLine(str, options)
end
function ftext.cfText(str, opts)
  local options = {}
  if opts == nil then
    opts = {}
  end
  if type(opts) == "table" then
    options = table.deepcopy(opts)
    options.formatType = "c"
  else
    error("Improper argument: options expected to be passed as table")
  end
  options = ftext.fixFormatOptions(str, options)
  return ftext.fText(str, options)
end
function ftext.dfText(str, opts)
  local options = {}
  if opts == nil then
    opts = {}
  end
  if type(opts) == "table" then
    options = table.deepcopy(opts)
    options.formatType = "d"
  else
    error("Improper argument: options expected to be passed as table")
  end
  options = ftext.fixFormatOptions(str, options)
  return ftext.fText(str, options)
end
function ftext.hfText(str, opts)
  local options = {}
  if opts == nil then
    opts = {}
  end
  if type(opts) == "table" then
    options = table.deepcopy(opts)
    options.formatType = "h"
  else
    error("Improper argument: options expected to be passed as table")
  end
  options = ftext.fixFormatOptions(str, options)
  return ftext.fText(str, options)
end
local TextFormatter = {}
TextFormatter.validFormatTypes = {'d', 'dec', 'decimal', 'h', 'hex', 'hexidecimal', 'c', 'color', 'colour', 'col', 'name', 'none', 'e', 'plain', ''}
function TextFormatter:setType(typeToSet)
  local isNotValid = not table.contains(self.validFormatTypes, typeToSet)
  if isNotValid then
    error("TextFormatter:setType: Invalid argument, valid types are:" .. table.concat(self.validFormatTypes, ", "))
  end
  self.options.formatType = typeToSet
end
function TextFormatter:toBoolean(thing)
  if type(thing) ~= "boolean" then
    if thing == "true" then
      thing = true
    elseif thing == "false" then
      thing = false
    else
      return nil
    end
  end
  return thing
end
function TextFormatter:checkString(str)
  if type(str) ~= "string" then
    if tostring(str) then
      str = tostring(str)
    else
      return nil
    end
  end
  return str
end
function TextFormatter:setWrap(shouldWrap)
  local argumentType = type(shouldWrap)
  shouldWrap = self:toBoolean(shouldWrap)
  if shouldWrap == nil then
    error("TextFormatter:setWrap(shouldWrap) Argument error, boolean expected, got " .. argumentType ..
            ", if you want to set the number of characters wide to format for, use setWidth()")
  end
  self.options.wrap = shouldWrap
end
function TextFormatter:setWidth(width)
  if type(width) ~= "number" then
    if tonumber(width) then
      width = tonumber(width)
    else
      error("TextFormatter:setWidth(width): Argument error, number expected, got " .. type(width))
    end
  end
  self.options.width = width
end
function TextFormatter:setCap(cap)
  local argumentType = type(cap)
  local cap = self:checkString(cap)
  if cap == nil then
    error("TextFormatter:setCap(cap): Argument error, string expect, got " .. argumentType)
  end
  self.options.cap = cap
end
function TextFormatter:setCapColor(capColor)
  local argumentType = type(capColor)
  local capColor = self:checkString(capColor)
  if capColor == nil then
    error("TextFormatter:setCapColor(capColor): Argument error, string expected, got " .. argumentType)
  end
  self.options.capColor = capColor
end
function TextFormatter:setSpacerColor(spacerColor)
  local argumentType = type(spacerColor)
  local spacerColor = self:checkString(spacerColor)
  if spacerColor == nil then
    error("TextFormatter:setSpacerColor(spacerColor): Argument error, string expected, got " .. argumentType)
  end
  self.options.spacerColor = spacerColor
end
function TextFormatter:setTextColor(textColor)
  local argumentType = type(textColor)
  local textColor = self:checkString(textColor)
  if textColor == nil then
    error("TextFormatter:setTextColor(textColor): Argument error, string expected, got " .. argumentType)
  end
  self.options.textColor = textColor
end
function TextFormatter:setSpacer(spacer)
  local argumentType = type(spacer)
  local spacer = self:checkString(spacer)
  if spacer == nil then
    error("TextFormatter:setSpacer(spacer): Argument error, string expect, got " .. argumentType)
  end
  self.options.spacer = spacer
end
function TextFormatter:setAlignment(alignment)
  local validAlignments = {"left", "right", "center"}
  if not table.contains(validAlignments, alignment) then
    error("TextFormatter:setAlignment(alignment): Argument error: Only valid arguments for setAlignment are 'left', 'right', or 'center'. You sent" ..
            alignment)
  end
  self.options.alignment = alignment
end
function TextFormatter:setInside(spacerInside)
  local argumentType = type(spacerInside)
  spacerInside = self:toBoolean(spacerInside)
  if spacerInside == nil then
    error("TextFormatter:setInside(spacerInside) Argument error, boolean expected, got " .. argumentType)
  end
  self.options.inside = spacerInside
end
function TextFormatter:setMirror(shouldMirror)
  local argumentType = type(shouldMirror)
  shouldMirror = self:toBoolean(shouldMirror)
  if shouldMirror == nil then
    error("TextFormatter:setMirror(shouldMirror): Argument error, boolean expected, got " .. argumentType)
  end
  self.options.mirror = shouldMirror
end
function TextFormatter:setNoGap(noGap)
  local argumentType = type(noGap)
  noGap = self:toBoolean(noGap)
  if noGap == nil then
    error("TextFormatter:setNoGap(noGap): Argument error, boolean expected, got " .. argumentType)
  end
  self.options.noGap = noGap
end
function TextFormatter:enableTruncate()
  self.options.truncate = true
end
function TextFormatter:disableTruncate()
  self.options.truncate = false
end
function TextFormatter:format(str)
  return ftext.fText(str, self.options)
end
function TextFormatter:new(options)
  if options == nil then
    options = {}
  end
  if options and type(options) ~= "table" then
    error("TextFormatter:new(options): Argument error, table expected, got " .. type(options))
  end
  local me = {}
  me.options = {formatType = "c", wrap = true, width = 80, cap = "", spacer = " ", alignment = "center", inside = true, mirror = false}
  for option, value in pairs(options) do
    me.options[option] = value
  end
  setmetatable(me, self)
  self.__index = self
  return me
end
ftext.TextFormatter = TextFormatter
local TableMaker = {
  headCharacter = "*",
  footCharacter = "*",
  edgeCharacter = "*",
  rowSeparator = "-",
  separator = "|",
  separateRows = true,
  colorReset = "<reset>",
  formatType = "c",
  printHeaders = true,
  autoEcho = false,
  title = "",
  printTitle = false,
  headerTitle = false,
  forceHeaderSeparator = false,
  autoEchoConsole = "main",
}
function TableMaker:checkPosition(position, func)
  if position == nil then
    position = 0
  end
  if type(position) ~= "number" then
    if tonumber(position) then
      position = tonumber(position)
    else
      error(func .. ": Argument error: position expected as number, got " .. type(position))
    end
  end
  return position
end
function TableMaker:insert(tbl, pos, item)
  if pos ~= 0 then
    table.insert(tbl, pos, item)
  else
    table.insert(tbl, item)
  end
end
function TableMaker:getColumn(position)
  position = position or #self.columns
  position = self:checkPosition(position, "TableMaker:getColumn(position)")
  return self.columns[position]
end
function TableMaker:addColumn(options, position)
  if options == nil then
    options = {}
  end
  if not type(options) == "table" then
    error("TableMaker:addColumn(options, position): Argument error: options expected as table, got " .. type(options))
  end
  local options = table.deepcopy(options)
  position = self:checkPosition(position, "TableMaker:addColumn(options, position)")
  options.width = options.width or 20
  options.name = options.name or ""
  local formatter = TextFormatter:new(options)
  self:insert(self.columns, position, formatter)
end
function TableMaker:deleteColumn(position)
  if position == nil then
    error("TableMaker:deleteColumn(position): Argument Error: position as number expected, got nil")
  end
  position = self:checkPosition(position)
  local maxColumn = #self.columns
  if position > maxColumn then
    error(
      "TableMaker:deleteColumn(position): Argument Error: position provided was larger than number of columns in the table. Number of columns: " ..
        #self.columns)
  end
  table.remove(self.columns, position)
end
function TableMaker:replaceColumn(options, position)
  if position == nil then
    error("TableMaker:replaceColumn(options, position): Argument error: position as number expected, got nil")
  end
  position = self:checkPosition(position)
  if type(options) ~= "table" then
    error("TableMaker:replaceColumn(options, position): Argument error: options as table expected, got " .. type(options))
  end
  if #self.columns < position then
    error(
      "TableMaker:replaceColumn(options, position): you cannot specify a position higher than the number of columns currently in the TableMaker. You sent:" ..
        position .. " and there are: " .. #self.columns .. "columns in the TableMaker")
  end
  options.width = options.width or 20
  options.name = options.name or ""
  local formatter = TextFormatter:new(options)
  self.columns[position] = formatter
end
function TableMaker:getRow(position)
  position = position or #self.rows
  position = self:checkPosition(position, "TableMaker:getRow(position)")
  return self.rows[position]
end
function TableMaker:addRow(columnEntries, position)
  local columnEntriesType = type(columnEntries)
  if columnEntriesType ~= "table" then
    error("TableMaker:addRow(columnEntries, position): Argument error, columnEntries expected as table, got " .. columnEntriesType)
  end
  for _, entry in ipairs(columnEntries) do
    local entryCheck = self:checkEntry(entry)
    if entryCheck == 0 then
      if type(entry) == "function" then
        error(
          "TableMaker:addRow(columnEntries, position): Argument Error, you provided a function for a columnEntry but it does not return a string. We need a string. It was entry number " ..
            _ .. "in columnEntries")
      else
        error("TableMaker:addRow(columnEntries, position): Argument error, columnEntries items expected as string, got:" .. type(entry))
      end
    end
  end
  position = self:checkPosition(position, "TableMaker:addRow(columnEntries, position)")
  self:insert(self.rows, position, columnEntries)
end
function TableMaker:deleteRow(position)
  if position == nil then
    error("TableMaker:deleteRow(position): Argument Error: position as number expected, got nil")
  end
  position = self:checkPosition(position, "TableMaker:deleteRow(position)")
  local maxRow = #self.rows
  if position > maxRow then
    error("TableMaker:deleteRow(position): Argument Error: position given was > the number of rows we have # of rows is:" .. maxRow)
  end
  table.remove(self.rows, position)
end
function TableMaker:replaceRow(columnEntries, position)
  if position == nil then
    error("TableMaker:replaceRow(columnEntries, position): ArgumentError: position expected as number, received nil")
  end
  position = self:checkPosition(position, "TableMaker:replaceRow(columnEntries, position)")
  if #self.rows < position then
    error(
      "TableMaker:replaceRow(columnEntries, position): position cannot be greater than the number of rows already in the tablemaker. You provided: " ..
        position .. " and there are " .. #self.rows .. "rows in the TableMaker")
  end
  for _, entry in ipairs(columnEntries) do
    local entryCheck = self:checkEntry(entry)
    if entryCheck == 0 then
      if type(entry) == "function" then
        error(
          "TableMaker:replaceRow(columnEntries, position): Argument Error: you provided a function for a columnEntry but it does not return a string. We need a string. It was entry number " ..
            _ .. "in columnEntries")
      else
        error("TableMaker:replaceRow(columnEntries, position): Argument error: columnEntries items expected as string, got:" .. type(entry))
      end
    end
  end
  self.rows[position] = columnEntries
end
function TableMaker:checkEntry(entry)
  local allowedTypes = {"string"}
  if self.allowPopups then
    table.insert(allowedTypes, "table")
  end
  local entryType = type(entry)
  if entryType == "function" then
    entryType = type(entry())
  end
  if table.contains(allowedTypes, entryType) then
    return entry
  else
    return 0
  end
end
function TableMaker:checkNumber(num)
  if num == nil then
    num = 0
  end
  if not tonumber(num) then
    num = 0
  end
  return tonumber(num)
end
function TableMaker:getCell(row, column)
  local rowType = type(row)
  local columnType = type(column)
  local maxRow = #self.rows
  local maxColumn = #self.columns
  local ae = "TableMaker:getCell(row, column): Argument error:"
  row = self:checkNumber(row)
  column = self:checkNumber(column)
  if row == 0 then
    if rowType ~= "number" then
      printError(f"{ae} row as number expected, got {rowType}", true, true)
    else
      printError(f"{ae} rows start at 1, and you asked for row 0", true, true)
    end
  elseif column == 0 then
    if columnType ~= "number" then
      printError(f"{ae} column as number expected, got {columnType}", true, true)
    else
      printError(f"{ae} columns start at 1, and you asked for column 0", true, true)
    end
  elseif row > maxRow then
    printError(f"{ae} row exceeds number of rows in table ({maxRow})")
  elseif column > maxColumn then
    printError(f"{ae} column exceeds number of columns in table ({maxColumn})", true, true)
  end
  return self.rows[row][column], self.columns[column]
end
function TableMaker:setCell(row, column, entry)
  local maxRow = #self.rows
  local maxColumn = #self.columns
  local ae = "TableMaker:setCell(row, column, entry): Argument Error:"
  row = self:checkNumber(row)
  if row == 0 then
    error(ae .. " row must be a number, you provided " .. type(row))
  end
  column = self:checkNumber(column)
  if column == 0 then
    error(ae .. " column must be a number, you provided " .. type(column))
  end
  if row > maxRow then
    error(ae .. " row is higher than the number of rows in the table. Highest row:" .. maxRow)
  end
  if column > maxColumn then
    error(ae .. " column is higher than the number of columns in the table. Highest column:" .. maxColumn)
  end
  local entryType = type(entry)
  entry = self:checkEntry(entry)
  if entry == 0 then
    if entryType == "function" then
      error(ae .. " entry was provided as a function, but does not return a string. We need a string in the end")
    else
      error("TableMaker:setCell(row, column, entry): Argument Error: entry must be a string, or a function which returns a string. You provided a " .. entryType)
    end
  end
  self.rows[row][column] = entry
end
function TableMaker:totalWidth()
  local width = 0
  local numberOfColumns = #self.columns
  local separatorWidth = string.len(self.separator)
  local edgeWidth = string.len(self.edgeCharacter) * 2
  for _, column in ipairs(self.columns) do
    width = width + column.options.width
  end
  separatorWidth = separatorWidth * (numberOfColumns - 1)
  width = width + edgeWidth + separatorWidth
  return width
end
function TableMaker:getType()
  local dec = {"d", "decimal", "dec"}
  local hex = {"h", "hexidecimal", "hex"}
  local col = {"c", "color", "colour", "col", "name"}
  if table.contains(dec, self.formatType) then
    return 'd'
  elseif table.contains(hex, self.formatType) then
    return 'h'
  elseif table.contains(col, self.formatType) then
    return 'c'
  else
    return ''
  end
end
function TableMaker:echo(message, echoType, ...)
  local fType = self:getType()
  local consoleType = type(self.autoEchoConsole)
  local console = ""
  if echoType == nil then
    echoType = ""
  end
  if consoleType == "string" then
    console = self.autoEchoConsole
  elseif consoleType == "nil" then
    console = "main"
  else
    console = self.autoEchoConsole.name
  end
  local functionName = string.format("%secho%s", fType, echoType)
  local func = _G[functionName]
  if echoType == "" then
    func(console, message)
  else
    func(console, message, ...)
  end
end
function TableMaker:scanRow(rowToScan)
  local row = table.deepcopy(rowToScan)
  local rowEntries = #row
  local numberOfColumns = #self.columns
  local columns = {}
  local linesInRow = 0
  local rowText = ""
  local ec = self.frameColor .. self.edgeCharacter .. self.colorReset
  local sep = self.separatorColor .. self.separator .. self.colorReset
  if rowEntries < numberOfColumns then
    local entriesNeeded = numberOfColumns - rowEntries
    for i = 1, entriesNeeded do
      table.insert(row, "")
    end
  end
  for index, formatter in ipairs(self.columns) do
    local str = row[index]
    local column = ""
    if type(str) == "function" then
      str = str()
    end
    column = formatter:format(str)
    table.insert(columns, column:split("\n"))
  end
  for _, rowLines in ipairs(columns) do
    if linesInRow < #rowLines then
      linesInRow = #rowLines
    end
  end
  for index, rowLines in ipairs(columns) do
    if #rowLines < linesInRow then
      local neededLines = linesInRow - #rowLines
      for i = 1, neededLines do
        table.insert(rowLines, self.columns[index]:format(""))
      end
    end
  end
  for i = 1, linesInRow do
    local thisLine = ec
    for index, column in ipairs(columns) do
      if index == 1 then
        thisLine = string.format("%s%s", thisLine, column[i])
      else
        thisLine = string.format("%s%s%s", thisLine, sep, column[i])
      end
    end
    thisLine = string.format("%s%s", thisLine, ec)
    if rowText == "" then
      rowText = thisLine
    else
      rowText = string.format("%s\n%s", rowText, thisLine)
    end
  end
  return rowText
end
function TableMaker:echoRow(rowToScan)
  local row = table.deepcopy(rowToScan)
  local rowEntries = #row
  local numberOfColumns = #self.columns
  local columns = {}
  local linesInRow = 0
  local ec = self.frameColor .. self.edgeCharacter .. self.colorReset
  local sep = self.separatorColor .. self.separator .. self.colorReset
  if rowEntries < numberOfColumns then
    local entriesNeeded = numberOfColumns - rowEntries
    for i = 1, entriesNeeded do
      table.insert(row, "")
    end
  end
  for index, formatter in ipairs(self.columns) do
    local str = row[index]
    local column = ""
    if type(str) == "function" then
      str = str()
    end
    if type(str) == "table" then
      str = str[1]
    end
    column = formatter:format(str)
    table.insert(columns, column:split("\n"))
  end
  for _, rowLines in ipairs(columns) do
    if linesInRow < #rowLines then
      linesInRow = #rowLines
    end
  end
  for index, rowLines in ipairs(columns) do
    if #rowLines < linesInRow then
      local neededLines = linesInRow - #rowLines
      for i = 1, neededLines do
        table.insert(rowLines, self.columns[index]:format(""))
      end
    end
  end
  for i = 1, linesInRow do
    self:echo(ec)
    for index, column in ipairs(columns) do
      local message = column[i]
      if index ~= 1 then
        self:echo(sep)
      end
      if type(row[index]) == "string" then
        self:echo(message)
      elseif type(row[index]) == "table" then
        local rowEntry = row[index]
        local echoType = ""
        if type(rowEntry[2]) == "string" then
          echoType = "Link"
        elseif type(rowEntry[2]) == "table" then
          echoType = "Popup"
        end
        self:echo(message, echoType, rowEntry[2], rowEntry[3], rowEntry[4] or true)
      end
    end
    self:echo(ec)
    self:echo("\n")
  end
end
function TableMaker:makeHeader()
  local totalWidth = self:totalWidth()
  local ec = self.frameColor .. self.edgeCharacter .. self.colorReset
  local sep = self.separatorColor .. self.separator .. self.colorReset
  local header = self.frameColor .. string.rep(self.headCharacter, totalWidth) .. self.colorReset
  local columnHeaders = ""
  if self.printHeaders then
    local columnEntries = {}
    for _, v in ipairs(self.columns) do
      table.insert(columnEntries, v:format(v.options.name))
    end
    local divWithNewlines = self.headerTitle and header or self:createRowDivider()
    divWithNewlines = "\n" .. divWithNewlines
    columnHeaders = string.format("\n%s%s%s%s", ec, table.concat(columnEntries, sep), ec, (self.separateRows or self.forceHeaderSeparator) and divWithNewlines or '')
  end
  local title = self:makeTitle(totalWidth, header)
  header = string.format("%s%s%s", header, title, columnHeaders)
  return header
end
function TableMaker:makeTitle(totalWidth, header)
  if not self.printTitle then
    return ""
  end
  local title = ftext.fText(self.title, {width = totalWidth, alignment = "center", cap = self.headCharacter, capColor = self.frameColor, inside = true, textColor = self.titleColor, formatType = self.formatType})
  title = string.format("\n%s\n%s", title, header)
  return title
end
function TableMaker:createRowDivider()
  local columnPieces = {}
  for _, v in ipairs(self.columns) do
    local piece = string.format("%s%s%s", self.separatorColor, string.rep(self.rowSeparator, v.options.width), self.colorReset)
    table.insert(columnPieces, piece)
  end
  local ec = self.frameColor .. self.edgeCharacter .. self.colorReset
  local sep = self.separatorColor .. self.separator .. self.colorReset
  return string.format("%s%s%s", ec, table.concat(columnPieces, sep), ec)
end
function TableMaker:setTitle(title)
  self.title = title
  if self.autoEcho then self:assemble() end
end
function TableMaker:setRowSeparator(char)
  self.rowSeparator = char
  if self.autoEcho then self:assemble() end
end
function TableMaker:setEdgeCharacter(char)
  self.edgeCharacter = char
  if self.autoEcho then self:assemble() end
end
function TableMaker:setFootCharacter(char)
  self.footCharacter = char
  if self.autoEcho then self:assemble() end
end
function TableMaker:setHeadCharacter(char)
  self.headCharacter = char
  if self.autoEcho then self:assemble() end
end
function TableMaker:setSeparator(char)
  self.separator = char
  if self.autoEcho then self:assemble() end
end
function TableMaker:setTitleColor(color)
  self.titleColor = color
  if self.autoEcho then self:assemble() end
end
function TableMaker:setSeparatorColor(color)
  self.separatorColor = color
  if self.autoEcho then self:assemble() end
end
function TableMaker:setFrameColor(color)
  self.frameColor = color
  if self.autoEcho then self:assemble() end
end
function TableMaker:enableForceHeaderSeparator()
  self.forceHeaderSeparator = true
  if self.autoEcho then self:assemble() end
end
function TableMaker:disableForceHeaderSeparator()
  self.forceHeaderSeparator = false
  if self.autoEcho then self:assemble() end
end
function TableMaker:enableHeaderTitle()
  self.headerTitle = true
  if self.autoEcho then self:assemble() end
end
function TableMaker:disableHeaderTitle()
  self.headerTitle = false
  if self.autoEcho then self:assemble() end
end
function TableMaker:enablePrintTitle()
  self.printTitle = true
  if self.autoEcho then self:assemble() end
end
function TableMaker:disablePrintTitle()
  self.printTitle = false
  if self.autoEcho then self:assemble() end
end
function TableMaker:enablePrintHeaders()
  self.printHeaders = true
  if self.autoEcho then self:assemble() end
end
function TableMaker:disablePrintHeaders()
  self.printHeaders = false
  if self.autoEcho then self:assemble() end
end
function TableMaker:enableRowSeparator()
  self.separateRows = true
  if self.autoEcho then self:assemble() end
end
function TableMaker:disableRowSeparator()
  self.separateRows = false
  if self.autoEcho then self:assemble() end
end
function TableMaker:enablePopups()
  self.autoEcho = true
  self.allowPopups = true
  if self.autoEcho then self:assemble() end
end
function TableMaker:enableAutoEcho()
  self.autoEcho = true
  self:assemble()
end
function TableMaker:disableAutoEcho()
  if self.allowPopups then
    error("TableMaker:disableAutoEcho(): you cannot disable autoEcho once you have enabled popups.")
  else
    self.autoEcho = false
  end
end
function TableMaker:enableAutoClear()
  self.autoClear = true
  if self.autoEcho then self:assemble() end
end
function TableMaker:disableAutoClear()
  self.autoClear = false
end
function TableMaker:setAutoEchoConsole(console)
  local funcName = "TableMaker:setAutoEchoConsole(console)"
  if console == nil then
    console = "main"
  end
  local consoleType = type(console)
  if consoleType ~= "string" and consoleType ~= "table" then
    error(funcName .. " ArgumentError: console as string or a Geyser MiniConsole or UserWindow expected, got " .. consoleType)
  elseif consoleType == "table" and not (console.type == "miniConsole" or console.type == "userwindow") then
    error(funcName .. " ArgumentError: console received was a table and may be a Geyser object, but console.type is not miniConsole, it is " ..
            console.type)
  end
  self.autoEchoConsole = console
  if self.autoEcho then self:assemble() end
end
function TableMaker:assemble()
  if self.allowPopups and self.autoEcho then
    self:popupAssemble()
  else
    return self:textAssemble()
  end
end
function TableMaker:popupAssemble()
  if self.autoClear then
    local console = self.autoEchoConsole
    if console and console ~= "main" then
      if type(console) == "table" then
        console = console.name
      end
      clearWindow(console)
    end
  end
  local divWithNewLines = string.format("%s\n", self:createRowDivider())
  local header = self:makeHeader() .. "\n"
  local footer = string.format("%s%s%s\n", self.frameColor, string.rep(self.footCharacter, self:totalWidth()), self.colorReset)
  self:echo(header)
  for _, row in ipairs(self.rows) do
    if _ ~= 1 and self.separateRows then
      self:echo(divWithNewLines)
    end
    self:echoRow(row)
  end
  self:echo(footer)
end
function TableMaker:textAssemble()
  local sheet = ""
  local rows = {}
  for _, row in ipairs(self.rows) do
    table.insert(rows, self:scanRow(row))
  end
  local divWithNewlines = string.format("\n%s\n", self:createRowDivider())
  local footer = string.format("%s%s%s", self.frameColor, string.rep(self.footCharacter, self:totalWidth()), self.colorReset)
  sheet = string.format("%s\n%s\n%s\n", self:makeHeader(), table.concat(rows, self.separateRows and divWithNewlines or "\n"), footer)
  if self.autoEcho then
    local console = self.autoEchoConsole or "main"
    if type(console) == "table" then
      console = console.name
    end
    if self.autoClear and console ~= "main" then
      clearWindow(console)
    end
    self:echo(sheet)
  end
  return sheet
end
function TableMaker:new(options)
  local funcName = "TableMaker:new(options)"
  local me = {}
  setmetatable(me, self)
  self.__index = self
  if options == nil then
    options = {}
  end
  if type(options) ~= "table" then
    error("TableMaker:new(options): ArgumentError: options expected as table, got " .. type(options))
  end
  local options = table.deepcopy(options)
  if options.allowPopups == true then
    options.autoEcho = true
  else
    options.allowPopups = false
  end
  local columns = false
  if options.columns then
    if type(options.columns) ~= "table" then
      error("TableMaker:new(options): option error: You provided an options.columns entry of type " .. type(options.columns) ..
              " and columns must a table with entries suitable for TableFormatter:addColumn().")
    end
    columns = table.deepcopy(options.columns)
    options.columns = nil
  end
  local rows = false
  if options.rows then
    if type(options.rows) ~= "table" then
      error("TableMaker:new(options): option error: You provided an options.rows entry of type " .. type(options.rows) ..
              " and rows must be a table with entrys suitable for TableFormatter:addRow()")
    end
    rows = table.deepcopy(options.rows)
    options.rows = nil
  end
  for option, value in pairs(options) do
    me[option] = value
  end
  local dec = {"d", "decimal", "dec"}
  local hex = {"h", "hexidecimal", "hex"}
  local col = {"c", "color", "colour", "col", "name"}
  if table.contains(dec, me.formatType) then
    me.frameColor = me.frameColor or "<255,255,255>"
    me.separatorColor = me.separatorColor or me.frameColor
    me.titleColor = me.titleColor or me.frameColor
    me.colorReset = "<r>"
  elseif table.contains(hex, me.formatType) then
    me.frameColor = me.frameColor or "#ffffff"
    me.separatorColor = me.separatorColor or me.frameColor
    me.titleColor = me.titleColor or me.frameColor
    me.colorReset = "#r"
  elseif table.contains(col, me.formatType) then
    me.frameColor = me.frameColor or "<white>"
    me.separatorColor = me.separatorColor or me.frameColor
    me.titleColor = me.titleColor or me.frameColor
    me.colorReset = "<reset>"
  else
    me.frameColor = ""
    me.separatorColor = ""
    me.titleColor = ""
    me.colorReset = ""
  end
  me.columns = {}
  me.rows = {}
  if columns then
    for _, column in ipairs(columns) do
      me:addColumn(column)
    end
  end
  if rows then
    for _, row in ipairs(rows) do
      me:addRow(row)
    end
  end
  return me
end
ftext.TableMaker = TableMaker
return ftext