--[[     cc10 Host     ]]--
--[[      by Dog       ]]--
--[[ aka HydrantHunter ]]--
--[[ pastebin CR92ZjdE ]]--
local cc10Hver = "1.1.15"
--[[
Tested with/requires:
  - Mincraft 1.6.4 or 1.7.10
  - ComputerCraft 1.63+
    - A Computer (standard or advanced) with a wireless modem, or a Wireless Turtle (standard or advanced)
    - cc10 Remote running on an Advanced Wireless Pocket Computer
]]--
local tArgs = { ... }
-- CONFIGURATION
-- Default Settings
local termX, termY = term.getSize()
local me = os.getComputerID()
local config = "/data/cc10hostcnf"
local ccSettings = {
                     side = "top",
                     outputType = "UD",
                     onState = 15,
                     restore = true,
                     name = "cc10host",
                     note = "short note",
                     color = "I",
                     password = false,
                     newColors = true,
                     loc = { dim = "Overworld", x = "No GPS Fix", y = "No GPS Fix", z = "No GPS Fix", },
                   }
local newCmdData = { }
local netSide, quit = "none", "NO"
local kernelState, lockState = true, false
local client, getColor
 -- Color Definitions
local white = colors.white
local black = colors.black
local gray = colors.gray
local silver = colors.lightGray
local orange = colors.orange
local red = colors.red
local green = colors.green
local blue = colors.blue
local sky = colors.lightBlue
if not term.isColor() then
  gray = colors.black
  silver = colors.white
  orange = colors.white
  red = colors.white
  green = colors.white
  blue = colors.black
  sky = colors.white
end
-- END CONFIGURATION

local function saveData()
  if not fs.exists("/data") then fs.makeDir("/data") end
  local devConfig = fs.open(config, "w") or error("saveData(): Cannot open " .. config .. " for writing", 0)
  devConfig.write(textutils.serialize(ccSettings))
  devConfig.close()
end

local function netReceive()
  newCmdData = { }
  while true do
    local id, message = rednet.receive()
    if tostring(message.program) == "cc10" then
      client = id
      newCmdData = message
      local cmdList = { "ON", "OFF", "rNO", "rYES", "locDim", "locX", "locY", "locZ", "name", "note", "lvl", "color", "lock", }
      if not lockState then
        for i = 1, #cmdList, 1 do
          if newCmdData.cmd == cmdList[i] then
            return newCmdData.cmd
          end
        end
        if newCmdData.cmd == "AD" or newCmdData.cmd == "QRY" or newCmdData.cmd == "set" then
          return newCmdData.action
        end
      else
        if newCmdData.cmd == "unlock" then
          return newCmdData.cmd
        elseif newCmdData.cmd == "QRY" then
          return newCmdData.action
        end
      end
    end
  end
end

do
  local colorBurst = {
                       P = "Purple",
                       B = "Blue",
                       S = "Sky",
                       G = "Green",
                       R = "Red",
                       O = "Orange",
                       N = "Brown",
                       I = "Silver",
                     }

  getColor = function(assignment)
    for v, w in pairs(colorBurst) do
      if assignment == tostring(v) then
        return w
      end
    end
  end
end

local function drawElement(x, y, w, h, txColor, bgColor, text)
  local deText = tostring(text)
  if bgColor then term.setBackgroundColor(bgColor) end
  if w > #deText or h > 1 then       -- We're 'drawing' something more than text
    for i = 1, h, 1 do
      term.setCursorPos(x, y + i - 1)
      term.write(string.rep(" ", w)) -- Draw the 'element' (box/rectangle/line-seg)
    end
  end
  if deText ~= "" and deText ~= "nil" then
    if txColor then term.setTextColor(txColor) end
    if w < #deText then w = #deText end -- Ensure minimum length
    local xW = (x + math.floor(w / 2)) - math.floor(#deText / 2) -- Center the text horizontally
    local xH = y + math.floor(h / 2)    -- Center the text vertically
    term.setCursorPos(xW, xH)
    term.write(deText)
  end
end

local function staticTermScreen()
  local hText = ccSettings.name .. " / # " .. os.getComputerID() .. " / " .. "cc10Host " .. cc10Hver
  term.setBackgroundColor(black)
  term.clear()
  drawElement(1, 1, termX, 1, white, blue, hText)
  drawElement(2, 3, 1, 1, gray, black, "Output side:  ")
  term.setTextColor(orange)
  term.write(ccSettings.side)
  drawElement(2, 4, 1, 1, gray, nil, "Output type:")
  drawElement(2, 5, 1, 1, nil, nil, "Output level:")
  drawElement(2, 7, 1, 1, nil, nil, "Lock state:")
  drawElement(2, 9, 1, 1, nil, nil, "Assigned Color:")
  drawElement(2, 11, 1, 1, nil, nil, "Restore output on restart: ")
end

local function drawTermScreen()
  term.setTextColor(sky)
  term.setCursorPos(16, 4)
  if ccSettings.outputType == "A" then
    term.write("Analog           ")
  elseif ccSettings.outputType == "UA" then
    term.write("Universal Analog ")
  elseif ccSettings.outputType == "D" then
    term.write("Digital          ")
  elseif ccSettings.outputType == "UD" then
    term.write("Universal Digital")
  elseif ccSettings.outputType == "PS" then
    term.write("Pulsed (short)   ")
  elseif ccSettings.outputType == "PL" then
    term.write("Pulsed (long)    ")
  end
  term.setCursorPos(16, 5)
  ccSettings.state = rs.getAnalogOutput(ccSettings.side)
  term.setTextColor(ccSettings.state == 0 and red or green)
  term.write(tostring(ccSettings.state) .. "  ")
  term.setCursorPos(14, 7)
  term.setTextColor(silver)
  term.write(tostring(lockState) .. "  ")
  term.setCursorPos(18, 9)
  term.setTextColor(white)
  local thisColor = getColor(ccSettings.color)
  term.write(thisColor .. string.rep(" ", 10))
  term.setCursorPos(29, 11)
  term.setTextColor(silver)
  term.write(tostring(ccSettings.restore) .. "  ")
end

local function reportStatus()
  local tempSettings = {
                         program = "cc10host",
                         cc = me,
                         name = ccSettings.name,
                         note = ccSettings.note,
                         color = ccSettings.color,
                         state = ccSettings.state,
                         onState = ccSettings.onState,
                         outputType = ccSettings.outputType,
                         restore = ccSettings.restore,
                         lockState = lockState,
                         quietCount = 0,
                         loc = { dim = ccSettings.loc.dim, x = ccSettings.loc.x, y = ccSettings.loc.y, z = ccSettings.loc.z, },
                       }
  if not rednet.isOpen(netSide) then
    rednet.open(netSide)
    rednet.host("cc10", ccSettings.name)
  end
  rednet.send(client, tempSettings)
end

local function updateStatus(newStatus)
  local QRY = "NO"
  ccSettings.state = rs.getAnalogOutput(ccSettings.side)
  local validCommands = {
                          rNO = function() ccSettings.restore = false saveData() reportStatus() end,
                          rYES = function() ccSettings.restore = true saveData() reportStatus() end,
                          locDim = function() ccSettings.loc.dim = newCmdData.action saveData() reportStatus() end,
                          locX = function() ccSettings.loc.x = newCmdData.action saveData() reportStatus() end,
                          locY = function() ccSettings.loc.y = newCmdData.action saveData() reportStatus() end,
                          locZ = function() ccSettings.loc.z = newCmdData.action saveData() reportStatus() end,
                          name = function() ccSettings.name = newCmdData.action saveData() reportStatus() end,
                          note = function() ccSettings.note = newCmdData.action saveData() reportStatus() end,
                          lvl = function() if ccSettings.state > 0 then rs.setAnalogOutput(ccSettings.side, newCmdData.action) end ccSettings.onState = newCmdData.action saveData() reportStatus() end,
                          color = function() ccSettings.color = newCmdData.action saveData() reportStatus() end,
                          lock = function() if ccSettings.password == newCmdData.action then lockState = true reportStatus() end end,
                          unlock = function() if ccSettings.password == newCmdData.action then lockState = false reportStatus() end end,
                        }
  for command, func in pairs(validCommands) do
    if tostring(command) == tostring(newStatus) then
      return func()
    end
  end
   -- Toggle outputType
  local validTypes = {
                       A = true,
                       UA = true,
                       D = true,
                       UD = true,
                       PS = true,
                       PL = true,
                     }
  if validTypes[tostring(newStatus)] then
    ccSettings.outputType = tostring(newStatus)
    if tostring(newStatus) == "D" or tostring(newStatus) == "UD" or tostring(newStatus:sub(1, 1)) == "P" then
      ccSettings.onState = 15
      local rsTmp = rs.getAnalogOutput(ccSettings.side)
      if rsTmp > 0 then
        ccSettings.onState = rsTmp
      else
        ccSettings.onState = 15
      end
    end
    saveData()
    reportStatus()
    return
  end
  if tostring(newStatus) == "ON" then
    if (ccSettings.outputType == "UA" or ccSettings.outputType == "UD") then
      newStatus = ccSettings.onState
    else
      newStatus = rs.getAnalogOutput(ccSettings.side)
    end
  elseif tostring(newStatus) == "OFF" then
    if (ccSettings.outputType == "UA" or ccSettings.outputType == "UD") then
      newStatus = 0
    else
      newStatus = rs.getAnalogOutput(ccSettings.side)
    end
  elseif tostring(newStatus) == "cc10QRY" then
    QRY = "YES"
    newStatus = rs.getAnalogOutput(ccSettings.side)
  end
  if not tonumber(newStatus) then
    return                             -- ignore invalid broadcasts
  elseif tonumber(newStatus) > 15 then
    newStatus = 15
  elseif tonumber(newStatus) < 0 then
    newStatus = 0
  end
  rs.setAnalogOutput(ccSettings.side, newStatus)
  ccSettings.state = rs.getAnalogOutput(ccSettings.side)
  if ccSettings.outputType == "PS" and newStatus > 0 then
    reportStatus()
    sleep(0.5)
    rs.setAnalogOutput(ccSettings.side, 0)
    newStatus = rs.getAnalogOutput(ccSettings.side)
  elseif ccSettings.outputType == "PL" and newStatus > 0 then
    reportStatus()
    sleep(2)
    rs.setAnalogOutput(ccSettings.side, 0)
    newStatus = rs.getAnalogOutput(ccSettings.side)
  end
  if newStatus > 0 and newStatus < 16 then ccSettings.onState = newStatus end
  if QRY == "NO" then saveData() end
  reportStatus()
end

local function foregroundShell()
  term.setBackgroundColor(black)
  term.setTextColor(white)
  term.clear()
  term.setCursorPos(1, 1)
  if fs.exists(tArgs[1]) then
	  shell.run(tArgs[1])
  end
  term.clear()
  term.setCursorPos(1, 1)
  shell.run("shell")
  rednet.close(netSide)
  term.clear()
  term.setCursorPos(1, 1)
  term.write("cc10Host is OFFLINE")
  term.setCursorPos(1, 3)
end

local function ccKernel()
  while kernelState do
    if not tArgs[1] then
      drawTermScreen()
    end
    local nextAction = netReceive()
    updateStatus(nextAction)
  end
end

local function firstRun()
  if not fs.exists("/data") then fs.makeDir("/data") end
  ccSettings.state = 0
  -- Set computer name
  term.clear()
  drawElement(2, 2, 1, 1, white, black, "Please name this device")
  drawElement(3, 3, 1, 1, nil, nil, "(8 characters or less)")
  while true do
    term.setCursorPos(2, 5)
    local newName = tostring(read())
    if newName ~= "" and newName ~= "nil" then
      if #newName > 8 then newName = newName:sub(1, 8) end
      ccSettings.name = newName
      break
    end
  end
  -- Set computer description
  term.clear()
  drawElement(2, 2, 22, 1, nil, nil, "Please type in a short")
  drawElement(2, 3, 22, 1, nil, nil, "device description")
  while true do
    term.setCursorPos(2, 5)
    local newDesc = tostring(read())
    if newDesc ~= "" and newDesc ~= "nil" then
      ccSettings.note = newDesc
      break
    end
  end
  -- Set computer label
  local ccLabel = os.getComputerLabel()
  if ccLabel == nil or ccLabel == "" then
    os.setComputerLabel(ccSettings.name)
  end
   -- Select analog or digital output
  term.clear()
  drawElement(2, 2, 1, 1, nil, nil, "Redstone Output Type")
  drawElement(2, 4, 1, 1, nil, nil, "A = Analog")
  drawElement(2, 5, 1, 1, nil, nil, "D = Digital")
  drawElement(2, 6, 1, 1, nil, nil, "UA = Universal Analog")
  drawElement(2, 7, 1, 1, nil, nil, "UD = Universal Digital")
  drawElement(2, 8, 1, 1, nil, nil, "PS = Pulsed (0.5 seconds)")
  drawElement(2, 9, 1, 1, nil, nil, "PL = Pulsed (2 seconds)")
  drawElement(2, 11, 1, 1, nil, nil, "(Universal ~ ALL ON/OFF)")
  while true do
    term.setCursorPos(2, 13)
    local newType = tostring(read())
    if newType ~= "" and newType ~= "nil" then
      if string.upper(newType):sub(1, 1) == "D" then
        ccSettings.outputType = "D"
        ccSettings.onState = 15
        break
      elseif string.upper(newType):sub(1, 2) == "UD" then
        ccSettings.outputType = "UD"
        ccSettings.onState = 15
        break
      elseif string.upper(newType):sub(1, 1) == "A" then
        ccSettings.outputType = "A"
        ccSettings.onState = 15
        break
      elseif string.upper(newType):sub(1, 2) == "UA" then
        ccSettings.outputType = "UA"
        ccSettings.onState = 15
        break
      elseif string.upper(newType):sub(1, 2) == "PS" then
        ccSettings.outputType = "PS"
        ccSettings.onState = 15
        ccSettings.restore = false
        break
      elseif string.upper(newType):sub(1, 2) == "PL" then
        ccSettings.outputType = "PL"
        ccSettings.onState = 15
        ccSettings.restore = false
        break
      end
    end
  end
  -- Select redstone output side
  term.clear()
  term.setTextColor(sky)
  term.setCursorPos(2, 2)
  -- Draw side list
  drawElement(2, 2, 1, 1, nil, nil, "Please select the")
  drawElement(2, 3, 1, 1, nil, nil, "redstone output side")
  local theseSides = { "top", "bottom", "left", "right", "front", "back" }
  for l, m in ipairs(theseSides) do                                           --
    term.setCursorPos(2, l + 4)                                               -- Side selection list for standard computers and turtles
    term.write(tostring(l) .. " = " .. string.upper(m):sub(1, 1) .. m:sub(2)) --
  end
  term.setCursorPos(2, 12)
  while true do
    local getSide = tonumber(read())
    if getSide ~= nil then
      if getSide > 0 and getSide < 7 then
        for n, o in ipairs(theseSides) do
          if getSide == n then
            ccSettings.side = o
          end
        end
        break
      end
    end
  end
   -- Select color
  local colorBurst = {
                       P = "Purple",
                       B = "Blue",
                       S = "Sky",
                       G = "Green",
                       R = "Red",
                       O = "Orange",
                       N = "Brown",
                       I = "Silver",
                     }
  term.clear()
  drawElement(2, 2, 1, 1, nil, nil, "Select a color")
  local ttY = 4
  for g, h in pairs(colorBurst) do
    drawElement(2, ttY, 1, 1, nil, nil, tostring(g) .. " = " .. h)
    ttY = ttY + 1
  end
  while true do
    term.setCursorPos(2, 13)
    local newColor = string.upper(tostring(read()))
    if newColor ~= "" and newColor ~= "nil" then
      for v, w in pairs(colorBurst) do
        if newColor == tostring(v) then
          ccSettings.color = tostring(v)
          break
        end
      end
    end
    if newColor ~= "" and newColor ~= "nil" then break end
  end
  -- Set password
  term.clear()
  drawElement(2, 2, 1, 1, nil, nil, "Would you like to password")
  drawElement(2, 3, 1, 1, nil, nil, "protect this device?")
  term.setCursorPos(2, 5)
  local lockQ = string.lower(read())
  if lockQ == "y" or lockQ == "yes" then
    term.setCursorPos(2, 7)
    term.write("Please enter a password...")
    term.setCursorPos(2, 9)
    local pass = read()
    if pass == "" or pass == nil or pass == "nil" then
      ccSettings.password = false
    else
      ccSettings.password = pass
    end
  end
  saveData()
end

local function initMe()
  if fs.exists("/data/cc10hostCfg") then fs.delete("/data/cc10hostCfg") end
  if fs.exists("/data/cc10hostconf") then fs.delete("/data/cc10hostconf") end
  term.setBackgroundColor(black)
  for _, side in ipairs(rs.getSides()) do
    if peripheral.isPresent(side) then
      local perp = peripheral.getType(side)
      if tostring(perp) == "modem" then
        if peripheral.call(side, "isWireless") == true then
          netSide = side
          break
        end
      end
    end
  end
  if netSide == "none" then
    term.clear()
    drawElement(2, 2, 17, 1, red, black, "No wireless")
    drawElement(2, 3, 17, 1, nil, nil, "modem detected!")
    drawElement(2, 5, 17, 1, nil, nil, "cc10 REQUIRES")
    drawElement(2, 6, 17, 1, nil, nil, "a wireless modem.")
    term.setCursorPos(1, 9)
    quit = "YES"
    return
  end
  if not fs.exists(config) then firstRun() end
  term.clear()
  drawElement(2, 2, 1, 1, white, nil, "Ingesting configuration data . . .")
  local cc10Config = fs.open(config, "r") or error("initMe(): Cannot open " .. config .. " for reading", 0)
  local cc10Cfg = cc10Config.readAll()
  ccSettings = textutils.unserialize(cc10Cfg)
  cc10Config.close()
  if not ccSettings.newColors then
    if ccSettings.color == "L" then
      ccSettings.color = "S"
    elseif ccSettings.color == "Y" then
      ccSettings.color = "I"
    end
    ccSettings.newColors = true
    saveData()
  end
  if ccSettings.restore then
    drawElement(2, 4, 1, 1, nil, nil, "Restoring state . . .")
    rs.setAnalogOutput(ccSettings.side, ccSettings.state)
  end
  if ccSettings.password then
    lockState = true
  else
    ccSettings.password = false
  end
  drawElement(2, 6, 1, 1, nil, nil, "Configuring hardware . . .")
  if not rednet.isOpen(netSide) then
    rednet.open(netSide)
  end
  rednet.host("cc10", ccSettings.name)
  if os.clock() < 5 then sleep(math.random(1, 3)) end  -- This is so every device doesn't report in at once
  drawElement(2, 8, 1, 1, nil, nil, "Acquiring GPS fix . . .")
  ccSettings.loc.x, ccSettings.loc.y, ccSettings.loc.z = gps.locate(2)
  if not ccSettings.loc.x then
    ccSettings.loc.x, ccSettings.loc.y, ccSettings.loc.z = "No GPS Fix", "No GPS Fix", "No GPS Fix"
  end
  staticTermScreen()
end

initMe()

if quit == "YES" then return end
if tArgs[1] then
	parallel.waitForAny(foregroundShell, ccKernel)
else
  ccKernel()
end