w = {}

-- APIs

-- properties
local data = { }
local data_name = nil
local data_handlers = { }

local device_handlers = {}

local event_handlers = {}

local monitors = {}
local monitor_textScale = 0.5

local page_handlers = {}
local page_endText = ""
local page_callbackDisplay
local page_callbackKey

local event_refreshPeriod_s = 3.0
local event_refreshTimerId = -1

local styles = {
  normal   = { front = colors.black    , back = colors.lightGray },
  good     = { front = colors.lime     , back = colors.lightGray },
  bad      = { front = colors.red      , back = colors.lightGray },
  disabled = { front = colors.gray     , back = colors.lightGray },
  help     = { front = colors.white    , back = colors.blue      },
  header   = { front = colors.orange   , back = colors.black     },
  control  = { front = colors.white    , back = colors.blue      },
  selected = { front = colors.black    , back = colors.lightBlue },
  warning  = { front = colors.white    , back = colors.red       },
  success  = { front = colors.white    , back = colors.lime      },
}

----------- Terminal & monitor support

local function setMonitorColorFrontBack(colorFront, colorBackground)
  term.setBackgroundColor(colorBackground)
  term.setTextColor(colorFront)
  if monitors ~= nil then
    for key, monitor in pairs(monitors) do
      monitor.setTextColor(colorFront)
      monitor.setBackgroundColor(colorBackground)
    end
  end
end

local function write(text)
  term.write(text)
  if monitors ~= nil then
    for key, monitor in pairs(monitors) do
      if key ~= data.radar_monitorIndex then
        monitor.write(text)
      end
    end
  end
end

local function getCursorPos()
  local x, y = term.getCursorPos()
  return x, y
end

local function setCursorPos(x, y)
  term.setCursorPos(x, y)
  if monitors ~= nil then
    for key, monitor in pairs(monitors) do
      if key ~= data.radar_monitorIndex then
        monitor.setCursorPos(x, y)
      end
    end
  end
end

local function getResolution()
  local sizeX, sizeY = term.getSize()
  return sizeX, sizeY
end

local function setColorNormal()
  w.setMonitorColorFrontBack(styles.normal.front, styles.normal.back)
end

local function setColorGood()
  w.setMonitorColorFrontBack(styles.good.front, styles.good.back)
end

local function setColorBad()
  w.setMonitorColorFrontBack(styles.bad.front, styles.bad.back)
end

local function setColorDisabled()
  w.setMonitorColorFrontBack(styles.disabled.front, styles.disabled.back)
end

local function setColorHelp()
  w.setMonitorColorFrontBack(styles.help.front, styles.help.back)
end

local function setColorHeader()
  w.setMonitorColorFrontBack(styles.header.front, styles.header.back)
end

local function setColorControl()
  w.setMonitorColorFrontBack(styles.control.front, styles.control.back)
end

local function setColorSelected()
  w.setMonitorColorFrontBack(styles.selected.front, styles.selected.back)
end

local function setColorWarning()
  w.setMonitorColorFrontBack(styles.warning.front, styles.warning.back)
end

local function setColorSuccess()
  w.setMonitorColorFrontBack(styles.success.front, styles.success.back)
end

local function clear(colorFront, colorBack)
  if colorFront == nil or colorBack == nil then
    w.setColorNormal()
  else
    w.setMonitorColorFrontBack(colorFront, colorBack)
  end
  term.clear()
  if monitors ~= nil then
    for key, monitor in pairs(monitors) do
      if key ~= data.radar_monitorIndex then
        monitor.clear()
      end
    end
  end
  w.setCursorPos(1, 1)
end

local function clearLine()
  term.clearLine()
  if monitors ~= nil then
    for key, monitor in pairs(monitors) do
      if key ~= data.radar_monitorIndex then
        monitor.clearLine()
      end
    end
  end
  local x, y = w.getCursorPos()
  w.setCursorPos(1, y)
end

local function writeLn(text)
  w.write(text)
  local x, y = w.getCursorPos()
  local xSize, ySize = w.getResolution()
  if y > ySize - 1 then
    y = 1
  end
  w.setCursorPos(1, y + 1)
end

local function writeCentered(y, text)
  local unused
  if text == nil then
    text = y
    unused, y = w.getCursorPos()
  end
  
  w.setCursorPos((51 - text:len()) / 2, y)
  term.write(text)
  if monitors ~= nil then
    for key, monitor in pairs(monitors) do
      if key ~= data.radar_monitorIndex then
        local xSize, ySize = monitor.getSize()
        if xSize ~= nil then
          monitor.setCursorPos((xSize - text:len()) / 2, y)
          monitor.write(text)
        end
      end
    end
  end
  w.setCursorPos(1, y + 1)
end

local function writeFullLine(text)
  w.write(text)
  local xSize, ySize = w.getResolution()
  local xCursor, yCursor = w.getCursorPos()
  for i = xCursor, xSize do
    w.write(" ")
  end
  w.setCursorPos(1, yCursor + 1)
end

----------- Page support

local function page_begin(text)
  w.clear()
  w.setCursorPos(1, 1)
  w.setColorHeader()
  w.clearLine()
  w.writeCentered(1, text)
  w.status_refresh()
  w.setCursorPos(1, 2)
  w.setColorNormal()
end

local function page_colors()
  w.clear(colors.white, colors.black)
  for key, value in pairs(colors) do
    local text = string.format("%12s", key)
    w.setMonitorColorFrontBack(colors.white, colors.black)
    w.write(text .. " ")
    w.setMonitorColorFrontBack(value, colors.black)
    w.write(" " .. text .. " ")
    w.setMonitorColorFrontBack(colors.black, value)
    w.write(" " .. text .. " ")
    w.setMonitorColorFrontBack(colors.white, value)
    w.write(" " .. text .. " ")
    w.setMonitorColorFrontBack(value, colors.white)
    w.write(" " .. text .. " ")
    w.writeLn("")
  end
  w.writeLn("")
  local index = 0
  for key, value in pairs(styles) do
    local text = string.format("%12s", key)
    if index % 2 == 0 then
      w.setMonitorColorFrontBack(colors.white, colors.black)
      w.write(text .. " ")
      w.setMonitorColorFrontBack(value.front, value.back)
      w.write(" " .. text .. " ")
    else
      w.setMonitorColorFrontBack(value.front, value.back)
      w.write(" " .. text .. " ")
      w.setMonitorColorFrontBack(colors.white, colors.black)
      w.write(text .. " ")
      w.writeLn("")
    end
    index = index + 1
  end
  w.setMonitorColorFrontBack(colors.white, colors.black)
end

local function page_end()
  w.setCursorPos(1, 18)
  w.setColorControl()
  w.writeFullLine(page_endText)
end

local function page_getCallbackDisplay()
  return page_callbackDisplay
end

local function page_register(index, callbackDisplay, callbackKey)
  page_handlers[index] = { display = callbackDisplay, key = callbackKey }
end

local function page_setEndText(text)
  page_endText = text
end

----------- Status line support

local status_clockTarget = -1 -- < 0 when stopped, < clock when elapsed, > clock when ticking
local status_isWarning = false
local status_text = ""
local function status_clear()
  if status_clockTarget > 0 then
    status_clockTarget = -1
    local xSize, ySize = w.getResolution()
    w.setCursorPos(1, ySize)
    w.setColorNormal()
    w.clearLine()
  end
end
local function status_isActive()
  return status_clockTarget > 0 and w.event_clock() < status_clockTarget
end
local function status_show(isWarning, text)
  if isWarning or not w.status_isActive() then
    if isWarning then
      status_clockTarget = w.event_clock() + 1.0
    else
      status_clockTarget = w.event_clock() + 0.5
    end
    status_isWarning = isWarning
    if text ~= nil then
      status_text = text
    else
      status_text = "???"
    end
    w.status_refresh()
  end
end
local function status_refresh()
  if status_clockTarget > 0 then
    local xSize, ySize = w.getResolution()
    w.setCursorPos(1, ySize)
    w.setColorNormal()
    w.clearLine()
    
    if w.event_clock() < status_clockTarget then
      if status_isWarning then
        w.setColorWarning()
      else
        w.setColorSuccess()
      end
      w.setCursorPos((xSize - status_text:len()) / 2, ySize)
      w.write(" " .. status_text .. " ")
      w.setColorNormal()
    else
      status_clockTarget = -1
    end
  end
end
local function status_showWarning(text)
  w.status_show(true, text)
end
local function status_showSuccess(text)
  w.status_show(false, text)
end
local function status_tick()
  if status_clockTarget > 0 and w.event_clock() > status_clockTarget then
    local xSize, ySize = w.getResolution()
    w.setCursorPos(1, ySize)
    w.setColorNormal()
    w.clearLine()
    status_clockTarget = -1
  end
end

----------- Formatting

local function format_float(value, nbchar)
  local str = "?"
  if value ~= nil then
    if type(value) == "number" then
      str = string.format("%g", value)
    else
      str = type(value)
    end
  end
  if nbchar ~= nil then
    str = string.sub("               " .. str, -nbchar)
  end
  return str
end

local function format_integer(value, nbchar)
  local str = "?"
  if value ~= nil then
    if type(value) == "number" then
      str = string.format("%d", math.floor(value))
    else
      str = type(value)
    end
  end
  if nbchar ~= nil then
    str = string.sub("               " .. str, -nbchar)
  end
  return str
end

local function format_boolean(value, strTrue, strFalse)
  if value ~= nil then
    if type(value) == "boolean" then
      if value then
        return strTrue
      else
        return strFalse
      end
    else
      return type(value)
    end
  end
  return "?"
end

local function format_string(value, nbchar)
  local str = "?"
  if value ~= nil then
    str = "" .. value
  end
  if nbchar ~= nil then
    if #str > math.abs(nbchar) then
      str = string.sub(str, 1, math.abs(nbchar) - 1) .. "~"
    else
      str = string.sub(str .. "                                                  ", 1, nbchar)
    end
  end
  return str
end

local function format_address(value)
  local str = "?"
  if value ~= nil then
    str = "" .. value
  end
  str = string.sub(str, 10, 100)
  return str
end

----------- Input controls

local function input_readInteger(currentValue)
  local inputAbort = false
  local input = w.format_integer(currentValue)
  if input == "0" then
    input = ""
  end
  local ignoreNextChar = false
  local x, y = w.getCursorPos()
  
  term.setCursorBlink(true)
  repeat
    w.status_tick()
    w.setCursorPos(x, y)
    w.setColorNormal()
    w.write(input .. "            ")
    input = string.sub(input, -9)
    w.setCursorPos(x + #input, y)
    
    local params = { os.pullEventRaw() }
    local eventName = params[1]
    local firstParam = params[2]
    if firstParam == nil then firstParam = "none" end
    if eventName == "key" then
      local keycode = params[2]
      
      if keycode >= 2 and keycode <= 10 then -- 1 to 9
        input = input .. w.format_string(keycode - 1)
        ignoreNextChar = true
      elseif keycode == 11 or keycode == 82 then -- 0 & keypad 0
        input = input .. "0"
        ignoreNextChar = true
      elseif keycode >= 79 and keycode <= 81 then -- keypad 1 to 3
        input = input .. w.format_string(keycode - 78)
        ignoreNextChar = true
      elseif keycode >= 75 and keycode <= 77 then -- keypad 4 to 6
        input = input .. w.format_string(keycode - 71)
        ignoreNextChar = true
      elseif keycode >= 71 and keycode <= 73 then -- keypad 7 to 9
        input = input .. w.format_string(keycode - 64)
        ignoreNextChar = true
      elseif keycode == 14 then -- Backspace
        input = string.sub(input, 1, string.len(input) - 1)
        ignoreNextChar = true
      elseif keycode == 211 then -- Delete
        input = ""
        ignoreNextChar = true
      elseif keycode == 28 then -- Enter
        inputAbort = true
        ignoreNextChar = true
      elseif keycode == 74 or keycode == 12 or keycode == 49 then -- - on numeric keypad or - on US top or n letter
        if string.sub(input, 1, 1) == "-" then
          input = string.sub(input, 2)
        else
          input = "-" .. input
        end
        ignoreNextChar = true
      elseif keycode == 78 then -- +
        if string.sub(input, 1, 1) == "-" then
          input = string.sub(input, 2)
        end
        ignoreNextChar = true
      else
        ignoreNextChar = false
        -- w.status_showWarning("Key " .. keycode .. " is not supported here")
      end
      
    elseif eventName == "char" then
      local character = params[2]
      if ignoreNextChar then
        ignoreNextChar = false
        -- w.status_showWarning("Ignored char #" .. string.byte(character) .. " '" .. character .. "'")
      elseif character >= '0' and character <= '9' then -- 0 to 9
        input = input .. character
      elseif character == '-' or character == 'n' or character == 'N' then -- - or N
        if string.sub(input, 1, 1) == "-" then
          input = string.sub(input, 2)
        else
          input = "-" .. input
        end
      elseif character == '+' or character == 'p' or character == 'P' then -- + or P
        if string.sub(input, 1, 1) == "-" then
          input = string.sub(input, 2)
        end
      else
        w.status_showWarning("Key '" .. character .. "' is not supported here (" .. string.byte(character) .. ")")
      end
      
    elseif eventName == "terminate" then
      inputAbort = true
      
    else
      local isSupported, needRedraw = w.event_handler(eventName, firstParam)
      if not isSupported then
        w.status_showWarning("Event '" .. eventName .. "', " .. firstParam .. " is unsupported")
      end
    end
  until inputAbort
  term.setCursorBlink(false)
  w.setCursorPos(1, y + 1)
  if input == "" or input == "-" then
    return currentValue
  else
    return tonumber(input)
  end
end

local function input_readText(currentValue)
  local inputAbort = false
  local input = w.format_string(currentValue)
  local ignoreNextChar = false
  local x, y = w.getCursorPos()
  
  term.setCursorBlink(true)
  repeat
    w.status_tick()
    -- update display clearing extra characters
    w.setCursorPos(x, y)
    w.setColorNormal()
    w.write(w.format_string(input, 37))
    -- truncate input and set caret position
    input = string.sub(input, -36)
    w.setCursorPos(x + #input, y)
    
    local params = { os.pullEventRaw() }
    local eventName = params[1]
    local firstParam = params[2]
    if firstParam == nil then firstParam = "none" end
    if eventName == "key" then
      local keycode = params[2]
      
      if keycode == 14 then -- Backspace
        input = string.sub(input, 1, string.len(input) - 1)
        ignoreNextChar = true
      elseif keycode == 211 then -- Delete
        input = ""
        ignoreNextChar = true
      elseif keycode == 28 then -- Enter
        inputAbort = true
        ignoreNextChar = true
      else
        ignoreNextChar = false
        -- w.status_showWarning("Key " .. keycode .. " is not supported here")
      end
      
    elseif eventName == "char" then
      local character = params[2]
      if ignoreNextChar then
        ignoreNextChar = false
        -- w.status_showWarning("Ignored char #" .. string.byte(character) .. " '" .. character .. "'")
      elseif character >= ' ' and character <= '~' then -- any ASCII table minus controls and DEL
        input = input .. character
      else
        w.status_showWarning("Key '" .. character .. "' is not supported here (" .. string.byte(character) .. ")")
      end
      
    elseif eventName == "terminate" then
      inputAbort = true
      
    else
      local isSupported, needRedraw = w.event_handler(eventName, firstParam)
      if not isSupported then
        w.status_showWarning("Event '" .. eventName .. "', " .. firstParam .. " is unsupported")
      end
    end
  until inputAbort
  term.setCursorBlink(false)
  w.setCursorPos(1, y + 1)
  if input == "" then
    return currentValue
  else
    return input
  end
end

local function input_readConfirmation(message)
  if message == nil then
    message = "Are you sure? (Y/n)"
  end
  w.status_showWarning(message)
  repeat
    local params = { os.pullEventRaw() }
    local eventName = params[1]
    local firstParam = params[2]
    if firstParam == nil then firstParam = "none" end
    if eventName == "key" then
      local keycode = params[2]
      
      if keycode == 28 then -- Return or Enter
        w.status_clear()
        return true
      end
      
    elseif eventName == "char" then
      local character = params[2]
      w.status_clear()
      if character == 'y' or character == 'Y' then -- Y
        return true
      else
        return false
      end
      
    elseif eventName == "terminate" then
      return false
      
    else
      local isSupported, needRedraw = w.event_handler(eventName, firstParam)
      if not isSupported then
        w.status_showWarning("Event '" .. eventName .. "', " .. firstParam .. " is unsupported")
      end
    end
    if not w.status_isActive() then
      w.status_showWarning(message)
    end
  until false
end

local function input_readEnum(currentValue, list, toValue, toDescription, noValue)
  local inputAbort = false
  local inputKey = nil
  local input = nil
  local inputDescription = nil
  local ignoreNextChar = false
  local x, y = w.getCursorPos()
  
  w.setCursorPos(1, 17)
  for key, entry in pairs(list) do
    if toValue(entry) == currentValue then
      inputKey = key
    end
  end
  
  term.setCursorBlink(true)
  repeat
    w.status_tick()
    w.setCursorPos(x, y)
    w.setColorNormal()
    if #list == 0 then
      inputKey = nil
    end
    if inputKey == nil then
      if currentValue ~= nil then
        input = noValue
        inputDescription = "Press enter to return previous entry"
      else
        input = noValue
        inputDescription = "Press enter to close listing"
      end
    else
      if inputKey < 1 then
        inputKey = #list
      elseif inputKey > #list then
        inputKey = 1
      end
      
      input = toValue(list[inputKey])
      inputDescription = toDescription(list[inputKey])
    end
    w.setColorNormal()
    w.write(input .. "                                                  ")
    w.setCursorPos(1, y + 1)
    w.setColorDisabled()
    w.write(inputDescription .. "                                                  ")
    
    local params = { os.pullEventRaw() }
    local eventName = params[1]
    local firstParam = params[2]
    if firstParam == nil then firstParam = "none" end
    if eventName == "key" then
      local keycode = params[2]
      
      if keycode == 14 or keycode == 211 then -- Backspace or Delete
        inputKey = nil
        ignoreNextChar = true
      elseif keycode == 200 or keycode == 203 or keycode == 78 then -- Up or Left or +
        if inputKey == nil then
          inputKey = 1
        else
          inputKey = inputKey - 1
        end
        ignoreNextChar = true
      elseif keycode == 208 or keycode == 205 or keycode == 74 then -- Down or Right or -
        if inputKey == nil then
          inputKey = 1
        else
          inputKey = inputKey + 1
        end
        ignoreNextChar = true
      elseif keycode == 28 then -- Enter
        inputAbort = true
        ignoreNextChar = true
      else
        ignoreNextChar = false
        -- w.status_showWarning("Key " .. keycode .. " is not supported here")
      end
      
    elseif eventName == "char" then
      local character = params[2]
      if ignoreNextChar then
        ignoreNextChar = false
        -- w.status_showWarning("Ignored char #" .. string.byte(character) .. " '" .. character .. "'")
      elseif character == '+' then -- +
        if inputKey == nil then
          inputKey = 1
        else
          inputKey = inputKey - 1
        end
      elseif character == '-' then -- -
        if inputKey == nil then
          inputKey = 1
        else
          inputKey = inputKey + 1
        end
      else
        w.status_showWarning("Key '" .. character .. "' is not supported here (" .. string.byte(character) .. ")")
      end
      
    elseif eventName == "terminate" then
      inputAbort = true
      
    elseif not w.event_handler(eventName, firstParam) then
      w.status_showWarning("Event '" .. eventName .. "', " .. firstParam .. " is unsupported")
    end
  until inputAbort
  term.setCursorBlink(false)
  w.setCursorPos(1, y + 1)
  w.clearLine()
  if inputKey == nil then
    return nil
  else
    return toValue(list[inputKey])
  end
end

----------- Event handlers

local function reboot()
  os.reboot()
end

local function sleep(delay)
  os.sleep(delay)
end

-- return a global clock measured in second
local function event_clock()
  return os.clock()
end

local function event_refresh_start()
  if event_refreshTimerId == -1 then
    event_refreshTimerId = os.startTimer(event_refreshPeriod_s)
  end
end

local function event_refresh_stop()
  if event_refreshTimerId ~= -1 then
    os.cancelTimer(event_refreshTimerId)
    event_refreshTimerId = -1
  end
end

local function event_refresh_tick()
  event_refreshTimerId = -1
  event_refresh_start()
end

local function event_register(eventName, callback)
  event_handlers[eventName] = callback
end

-- returns isSupported, needRedraw
local function event_handler(eventName, param)
  local needRedraw = false
  if eventName == "redstone" then
    w.redstone_event(param)
  elseif eventName == "timer" then
    if param == event_refreshTimerId then
      w.event_refresh_tick()
      needRedraw = page_callbackDisplay ~= page_handlers['0'].display
    else
      w.status_showWarning("Timer #" .. param .. " is unknown")
    end
  elseif eventName == "key_up" then
  elseif eventName == "mouse_click" then
    w.status_showSuccess("Use the keyboard, Luke!")
  elseif eventName == "mouse_up" then
  elseif eventName == "mouse_drag" then
  elseif eventName == "mouse_scroll" then
  elseif eventName == "monitor_touch" then
  elseif eventName == "monitor_resize" then
  elseif eventName == "disk" then
  elseif eventName == "disk_eject" then
  elseif eventName == "peripheral" then
  elseif eventName == "peripheral_detach" then
  -- not supported: task_complete, rednet_message, modem_message
  elseif event_handlers[eventName] ~= nil then
    needRedraw = event_handlers[eventName](eventName, param)
  else
    return false, needRedraw
  end
  return true, needRedraw
end

----------- Redstone support

local tblRedstoneState = {-- Remember redstone state on each side
  ["top"] = rs.getInput("top"),
  ["front"] = rs.getInput("front"),
  ["left"] = rs.getInput("left"),
  ["right"] = rs.getInput("right"),
  ["back"] = rs.getInput("back"),
  ["bottom"] = rs.getInput("bottom"),
}

local function redstone_event()
  -- Event only returns nil so we need to check sides manually
  local message = ""
  for side, state in pairs(tblRedstoneState) do
    if rs.getInput(side) ~= state then
      -- print(side .. " is now " .. tostring(rs.getInput(side)))
      message = message .. side .. " "
      tblRedstoneState[side] = rs.getInput(side)
    end
  end
  if message ~= "" then
    message = "Redstone changed on " .. message
    w.status_showWarning(message)
  end
end

----------- Configuration

local function data_get()
  return data
end

local function data_inspect(key, value)
  local stringValue = type(value) .. ","
  if type(value) == "boolean" then
    if value then
      stringValue = "true,"
    else
      stringValue = "false,"
    end
  elseif type(value) == "number" then
    stringValue = value .. ","
  elseif type(value) == "string" then
    stringValue = "'" .. value .. "',"
  elseif type(value) == "table" then
    stringValue = "{"
  end
  print(" " .. key .. " = " .. stringValue)
  if type(value) == "table" then
    for subkey, subvalue in pairs(value) do
      w.data_inspect(subkey, subvalue)
    end
    print("}")
  end
end

local function data_read()
  w.data_shouldUpdateName()
  
  data = { }
  if fs.exists("shipdata.txt") then
    local size = fs.getSize("shipdata.txt")
    if size > 0 then
      local file = fs.open("shipdata.txt", "r")
      if file ~= nil then
        local rawData = file.readAll()
        if rawData ~= nil then
          data = textutils.unserialize(rawData)
        end
        file.close()
        if data == nil then
          data = {}
        end
      end
    end
  end
  
  for name, handlers in pairs(data_handlers) do
    handlers.read(data)
  end
end

local function data_save()
  for name, handlers in pairs(data_handlers) do
    handlers.save(data)
  end
  
  local file = fs.open("shipdata.txt", "w")
  if file ~= nil then
    file.writeLine(textutils.serialize(data))
    file.close()
  else
    w.status_showWarning("No file system")
    w.sleep(3.0)
  end
end

local function data_getName()
  if data_name ~= nil then
    return data_name
  else
    return "-noname-"
  end
end

local function data_setName()
  -- check if any named component is connected
  local component = "computer"
  for name, handlers in pairs(data_handlers) do
    if handlers.name ~= nil then
      component = name
    end
  end
  
  -- ask for a new name
  w.page_begin("<==== Set " .. component .. " name ====>")
  w.setCursorPos(1, 4)
  w.setColorHelp()
  w.writeFullLine(" Press enter to validate.")
  w.setCursorPos(1, 3)
  w.setColorNormal()
  w.write("Enter " .. component .. " name: ")
  data_name = w.input_readText(data_name)
  
  -- update computer name
  os.setComputerLabel(data_name)
  
  -- update connected components
  for name, handlers in pairs(data_handlers) do
    if handlers.name ~= nil then
      handlers.name(data_name)
    end
  end
  
  -- w.reboot() -- not needed
end

local function data_shouldUpdateName()
  local shouldUpdateName = false
  
  -- check computer name
  data_name = os.getComputerLabel()
  if data_name == nil then
    shouldUpdateName = true
    data_name = "" .. os.getComputerID()
  end
  
  -- check connected components names
  for name, handlers in pairs(data_handlers) do
    if handlers.name ~= nil then
      local componentName = handlers.name()
      if componentName == "default" or componentName == "" then
        shouldUpdateName = true
      elseif shouldUpdateName then
        data_name = componentName
      elseif data_name ~= componentName then
        shouldUpdateName = true
        data_name = componentName
      end
    end
  end
  
  return shouldUpdateName
end

local function data_splitString(source, sep)
  local sep = sep or ":"
  local fields = {}
  local pattern = string.format("([^%s]+)", sep)
  source:gsub(pattern, function(c) fields[#fields + 1] = c end)
  return fields
end

local function data_register(name, callbackRead, callbackSave, callbackName)
  -- read/save callbacks are always defined
  if callbackRead == nil then
    callbackRead = function() end
  end
  if callbackSave == nil then
    callbackSave = function() end
  end
  
  -- name callback is nil when not defined
  
  data_handlers[name] = { read = callbackRead, save = callbackSave, name = callbackName }
end

----------- Devices

local function device_get(address)
  return peripheral.wrap(address)
end

local function device_getMonitors()
  return monitors
end

local function device_register(deviceType, callbackRegister, callbackUnregister)
  device_handlers[deviceType] = { register = callbackRegister, unregister = callbackUnregister }
end

----------- Main loop


local function boot()
  if not term.isColor() then
    print("Advanced computer required")
    error()
  end
  print("loading...")
  
  math.randomseed(os.time())
  
  -- read configuration
  w.data_read()
  w.clear()
  print("data_read...")
  
  -- initial scanning
  monitors = {}
  w.page_begin(data_name .. " - Connecting...")
  w.writeLn("")
  
  local sides = peripheral.getNames()
  for key, address in pairs(sides) do
    w.sleep(0)
    w.write("Checking " .. address .. " ")
    local deviceType = peripheral.getType(address)
    w.write(deviceType .. " ")
    if deviceType == "monitor" then
      w.write("wrapping!")
      local lmonitor = w.device_get(address)
      table.insert(monitors, lmonitor)
      lmonitor.setTextScale(monitor_textScale)
    else
      local handlers = device_handlers[deviceType]
      if handlers ~= nil then
        w.write("wrapping!")
        handlers.register(deviceType, address, w.device_get(address))
      end
    end
    
    w.writeLn("")
  end
  
  -- synchronize computer and connected components names
  local shouldUpdateName = w.data_shouldUpdateName()
  if shouldUpdateName then
    w.data_setName()
  end
  
  -- peripheral boot up
  if page_handlers['0'] == nil then
    w.status_showWarning("Missing handler for connection page '0'!")
    error()
  end
  page_handlers['0'].display(true)
end

local function run()
  local abort = false
  local refresh = true
  local ignoreNextChar = false
  
  local function selectPage(index)
    if page_handlers[index] ~= nil then
      page_callbackDisplay = page_handlers[index].display
      page_callbackKey = page_handlers[index].key
      refresh = true
      return true
    end
    return false
  end
  
  -- start refresh timer
  w.event_refresh_start()
  
  -- main loop
  selectPage('0')
  repeat
    w.status_tick()
    if refresh then
      w.clear()
      page_callbackDisplay(false)
      w.page_end()
      refresh = false
    end
    local params = { os.pullEventRaw() }
    local eventName = params[1]
    local firstParam = params[2]
    if firstParam == nil then firstParam = "none" end
    -- w.writeLn("...")
    -- w.writeLn("Event '" .. eventName .. "', " .. firstParam .. " received")
    -- w.sleep(0.2)
    
    if eventName == "key" then
      local keycode = params[2]
      
      ignoreNextChar = false
      if keycode == 11 or keycode == 82 then -- 0
        if selectPage('0') then
          ignoreNextChar = true
        end
      elseif keycode == 2 or keycode == 79 then -- 1
        if selectPage('1') then
          ignoreNextChar = true
        end
      elseif keycode == 3 or keycode == 80 then -- 2
        if selectPage('2') then
          ignoreNextChar = true
        end
      elseif keycode == 4 or keycode == 81 then -- 3
        if selectPage('3') then
          ignoreNextChar = true
        end
      elseif keycode == 5 or keycode == 82 then -- 4
        if selectPage('4') then
          ignoreNextChar = true
        end
      elseif keycode == 6 or keycode == 83 then -- 5
        if selectPage('5') then
          ignoreNextChar = true
        end
      elseif page_callbackKey ~= nil and page_callbackKey("", keycode) then
        refresh = true
      else
        ignoreNextChar = false
        -- w.status_showWarning("Key " .. keycode .. " is not supported here")
      end
      
    elseif eventName == "char" then
      local character = params[2]
      if ignoreNextChar then
        ignoreNextChar = false
        -- w.status_showWarning("Ignored char #" .. string.byte(character) .. " '" .. character .. "'")
--      elseif character == 'x' or character == 'X' then -- x for eXit
--        -- os.pullEventRaw() -- remove key_up event
--        abort = true
      elseif character == '0' then
        selectPage('0')
      elseif character == '1' then
        selectPage('1')
      elseif character == '2' then
        selectPage('2')
      elseif character == '3' then
        selectPage('3')
      elseif character == '4' then
        selectPage('4')
      elseif character == '5' then
        selectPage('5')
      elseif page_callbackKey ~= nil and page_callbackKey(character, -1) then
        refresh = true
      elseif string.byte(character) ~= 0 then -- not a control char
        w.status_showWarning("Key '" .. character .. "' is not supported here (" .. string.byte(character) .. ")")
      end
      
    elseif eventName == "terminate" then
      abort = true
      
    else
      local isSupported, needRedraw = w.event_handler(eventName, firstParam)
      if not isSupported then
        w.status_showWarning("Event '" .. eventName .. "', " .. firstParam .. " is unsupported")
      end
      refresh = needRedraw
    end
  until abort
  
  -- stop refresh timer
  w.event_refresh_stop()
end

local function close()
  w.clear(colors.white, colors.black)
  for key, handlers in pairs(device_handlers) do
    w.writeLn("Closing " .. key)
    if handlers.unregister ~= nil then
      handlers.unregister(key)
    end
  end
  
  w.clear(colors.white, colors.black)
  w.setCursorPos(1, 1)
  w.writeLn("Program closed")
  w.writeLn("Type startup to return to home page")
end

-- we simulate LUA library behavior, so code is closer between CC and OC
w = {
  setMonitorColorFrontBack = setMonitorColorFrontBack,
  write = write,
  getCursorPos = getCursorPos,
  setCursorPos = setCursorPos,
  getResolution = getResolution,
  setColorNormal = setColorNormal,
  setColorGood = setColorGood,
  setColorBad = setColorBad,
  setColorDisabled = setColorDisabled,
  setColorHelp = setColorHelp,
  setColorHeader = setColorHeader,
  setColorControl = setColorControl,
  setColorSelected = setColorSelected,
  setColorWarning = setColorWarning,
  setColorSuccess = setColorSuccess,
  clear = clear,
  clearLine = clearLine,
  writeLn = writeLn,
  writeCentered = writeCentered,
  writeFullLine = writeFullLine,
  page_begin = page_begin,
  page_colors = page_colors,
  page_end = page_end,
  page_getCallbackDisplay = page_getCallbackDisplay,
  page_register = page_register,
  page_setEndText = page_setEndText,
  status_clear = status_clear,
  status_isActive = status_isActive,
  status_show = status_show,
  status_refresh = status_refresh,
  status_showWarning = status_showWarning,
  status_showSuccess = status_showSuccess,
  status_tick = status_tick,
  format_float = format_float,
  format_integer = format_integer,
  format_boolean = format_boolean,
  format_string = format_string,
  format_address = format_address,
  input_readInteger = input_readInteger,
  input_readText = input_readText,
  input_readConfirmation = input_readConfirmation,
  input_readEnum = input_readEnum,
  reboot = reboot,
  sleep = sleep,
  event_clock = event_clock,
  event_refresh_start = event_refresh_start,
  event_refresh_stop = event_refresh_stop,
  event_refresh_tick = event_refresh_tick,
  event_register = event_register,
  event_handler = event_handler,
  data_get = data_get,
  data_inspect = data_inspect,
  data_read = data_read,
  data_save = data_save,
  data_getName = data_getName,
  data_setName = data_setName,
  data_shouldUpdateName = data_shouldUpdateName,
  data_splitString = data_splitString,
  data_register = data_register,
  device_get = device_get,
  device_getMonitors = device_getMonitors,
  device_register = device_register,
  boot = boot,
  run = run,
  close = close,
}
