-- [ --------------------------------------------------------------------- ] --
-- [ RedNetChat - A public chat program for ComputerCraft ] --
-- [ Created by gpgautier (ign gpgauier), 2012 ] --
-- [ Licence: Creative Commons Attribution-ShareAlike 3.0 Unported License ] --
-- [ ComputerCraft version: 1.3 and up ] --
-- [ Posted to: ] --
-- [ GitHub: https://github.com/gpgautier/RedNetChat ] --
-- [ Pastebin: http://pastebin.com/JDU4wJxX ] --
-- [ --------------------------------------------------------------------- ] --

local VERSION = "0.6"
local MODEM = nil
local NICKNAME = nil
local ACTIVE = false
local BUFFER = {}
local POINTER = 0
local ONLINE = {}
local ISONLINE = false
local ID = os.computerID()
local LAST_MSG_TARGET = nil
local CHANNEL = 1
local SCROLL_POINTER = POINTER
local WIDTH, HEIGHT = term.getSize()
local LINES = HEIGHT - 6
local START_LINE = 5
local OPERATOR = "RNC"

-- [ --------------------------------------------------------------------- ] --

-- Split a string
function split(str, pat)
local t = {} -- NOTE: use {n = 0} in Lua-5.0
if str ~= nil then
local fpat = "(.-)" .. pat
local last_end = 1
local s, e, cap = str:find(fpat, 1)
while s do
if s ~= 1 or cap ~= "" then
table.insert(t,cap)
end
last_end = e+1
s, e, cap = str:find(fpat, last_end)
end
if last_end <= #str then
cap = str:sub(last_end)
table.insert(t, cap)
end
else
print("##ERROR failed to split ["..str.."] by:"..pat)
end
return t
end

-- Log a message to file
function log(message)
local file = io.open("rednetchat.log", "a")
file:write("\n" .. message)
file:close()
end

-- Application entry
function main()
term.clear()
term.setCursorPos(1, 1)

if not setPeripherals() then
print("[FATAL ERROR] Not able to setup peripherals.")
return false
end

welcome()
end

-- Set the attached peripherals. Opens rednet modem and warps monitor
function setPeripherals()
local i, side

for i, side in pairs(rs.getSides()) do
if peripheral.isPresent(side) then
if peripheral.getType(side) == "modem" then
MODEM = side
if not rednet.isOpen(side) then
rednet.open(MODEM)
end
end
end
end

-- Exit with a fatal error when modem not found
if MODEM == nil then
print("[FATAL ERROR] No modem was detected. Plase attach a modem on any side.")
return false
end

return true
end

-- Start the welcome screen
function welcome()
local x, y

term.clear()
writeHeader()

print("")
print("")
print("Enter a nickname and press [enter].")
print("")
term.write("Nickname: ")

x, y = term.getCursorPos()

while NICKNAME == nil or NICKNAME == "" do
term.setCursorPos(x, y)
NICKNAME = read()
execute("/online")
appendBuffer("[" .. OPERATOR .. "]: Type /help for a list of commands")
end

start()
end

-- Writes the screen header
function writeHeader()
local col

term.setCursorPos(1, 1)
term.write("LeOS Chat " .. VERSION .. "")
term.setCursorPos(1, 2)

for col = 1, WIDTH do
term.write("-")
end
end

-- Writes the list of online users
function writeOnlineList()
local i, v, count, x, y, col

count = 0

x, y = term.getCursorPos()

term.setCursorPos(1, HEIGHT - 1)

for col = 1, WIDTH do
term.write("-")
end

term.setCursorPos(1, HEIGHT)
term.clearLine()
term.write("Online: ")

for i, v in pairs(ONLINE) do
if count == 0 then
term.write(i)
else
term.write(", " .. i)
end

count = count + 1
end

if count == 0 then
term.write("Nobody online in channel " .. CHANNEL)
end

term.setCursorPos(x, y)
end

-- Start the chat
function start()
term.clear()
writeHeader()
writeOnlineList()

ACTIVE = true

showBuffer()

parallel.waitForAll(input, watchEvents)
end

-- Stop the application
function stop()
ACTIVE = false
end

-- Reset the application
function reset()
execute("/offline")

if rednet.isOpen(MODEM) then
rednet.close(MODEM)
end

sleep(1.5)
os.reboot()
end

-- Watch all input to provide possible shortcuts (for example usernames)
function watchEvents()
local type, param, param2, param3, i, v

while ACTIVE do
type, param, param2, param3 = os.pullEvent()

if type == "key" then
if param == 200 then -- up
scroll(-1)
elseif param == 208 then -- down
scroll(1)
elseif param == 201 then -- pageup
scroll(-12)
elseif param == 209 then -- pagedown
scroll(12)
--else
-- appendBuffer(tostring(param))
end
elseif type == "mouse_scroll" then
if param == -1 then
scroll(-1)
else
scroll(1)
end
elseif type == "rednet_message" then
receive(param2)
end
end
end

-- Scroll through the chat
function scroll(amount)
SCROLL_POINTER = SCROLL_POINTER + amount
showBuffer()
end

-- Handle input from the prompt
function input()
local message, col

term.setCursorPos(1, 4)

for col = 1, WIDTH do
term.write("-")
end

while ACTIVE do
term.setCursorPos(1, 3)
term.clearLine()
term.write("[" .. CHANNEL .. "] > ")

message = read()

if message ~= nil and message ~= "" then
execute(message, "local")
end
end
end

-- Send a message
function send(message, target)
local request, serialized, x, encrypted

request = {protocol = "rnc", nickname = NICKNAME, sender = ID, target = target, channel = CHANNEL, message = message}
serialized = textutils.serialize(request)

encrypted = ""
for x = 1, #serialized do
encrypted = encrypted .. string.char(serialized:byte(x) + 1)
end

if request.target ~= nil then
rednet.send(request.target, encrypted)
else
rednet.broadcast(encrypted)
end
end

-- Recieve a message
function receive(message)
local request, decrypted, x

if message ~= nil and message ~= "" then

decrypted = ""
for x = 1, #message do
decrypted = decrypted .. string.char(message:byte(x) - 1)
end

request = textutils.unserialize(decrypted)

if request.protocol == "rnc" and request.channel == CHANNEL then
if request.nickname ~= nil and request.nickname ~= "" then
execute(request, "remote")
end
end
end
end

-- Execute a command or add a chat message
function execute(message, source)
local command, splitCommand, nickname, id, body, onlineUser

if message.nickname ~= nil then
executeRemote(message)
return
end

if message:sub(0, 1) == "/" then
command = message:sub(2)

if command == "quit"
or command == "reset"
or command == "restart"
or command == "reboot"
or command == "stop"
then
appendBuffer("[" .. OPERATOR .. "]: Stopping application")
reset()
elseif command == "online" then
if not ISONLINE then
send("/online")
putOnline()
appendBuffer("[" .. OPERATOR .. "]: You are now online")
ISONLINE = true
else
appendBuffer("[" .. OPERATOR .. "]: You are already online")
end
elseif command == "offline" then
if ISONLINE then
send("/offline")
takeOffline()
appendBuffer("[" .. OPERATOR .. "]: You are now offline")
ISONLINE = false
else
appendBuffer("[" .. OPERATOR .. "]: You are already offline")
end
elseif command:sub(0, 5) == "nick " then
takeOffline()
NICKNAME = command:sub(6)
putOnline()
appendBuffer("[" .. OPERATOR .. "]: Your nickname has been changed")
elseif command:sub(0, 5) == "slap " then
appendBuffer(command:sub(6) .. " was slapped by " .. NICKNAME)
elseif command:sub(0, 4) == "msg " then
splitCommand = split(command:sub(5), "%s")

onlineUser = false

for nickname, id in pairs(ONLINE) do
if nickname == splitCommand[1] then
body = command:sub(5 + splitCommand[1]:len() + 1)
send(body, id)
appendBuffer(NICKNAME .. " > " .. nickname .. ": " .. body)
onlineUser = true
LAST_MSG_TARGET = nickname
end
end

if not onlineUser then
appendBuffer("[" .. OPERATOR .. "]: User " .. splitCommand[1] .. " is not online")
end
elseif command:sub(0, 2) == "r " then
if LAST_MSG_TARGET ~= nil then
execute("/msg " .. LAST_MSG_TARGET .. " " .. command:sub(3), "local")
else
appendBuffer("[" .. OPERATOR .. "]: No valid user for message")
end
elseif command:sub(0, 5) == "join " then
if CHANNEL ~= tonumber(command:sub(6)) then
execute("/offline")
CHANNEL = tonumber(command:sub(6))
execute("/online")
appendBuffer("[" .. OPERATOR .. "]: Joined channel " .. CHANNEL)
else
appendBuffer("[" .. OPERATOR .. "]: Already in channel " .. CHANNEL)
end
elseif command == "help" then
appendBuffer("[" .. OPERATOR .. "] Commands:")
appendBuffer("/quit : Exit the chat")
appendBuffer("/msg <nickname> <message> : Send a private message")
appendBuffer("/r <message> : Reply to a private message")
appendBuffer("/join <channel> : Switch channel")
else
appendBuffer("[" .. OPERATOR .. "]: Unknown command")
end

return
end

appendBuffer(NICKNAME .. ": " .. message)
send(message)
end

--
function putOnline(nickname, id)
if nickname == nil or id == nil then
nickname = NICKNAME
id = ID
end

ONLINE[nickname] = id

writeOnlineList()
end

--
function takeOffline(nickname, id)
if nickname == nil or id == nil then
nickname = NICKNAME
id = ID
end

ONLINE[nickname] = nil

writeOnlineList()
end

--
function executeRemote(request)
local command

if request.message:sub(0, 1) == "/" then
command = request.message:sub(2)

if command == "online" then
putOnline(request.nickname, request.sender)
appendBuffer("[" .. OPERATOR .. "]: " .. request.nickname .. " is now online")
send("/metoo")
elseif command == "offline" then
takeOffline(request.nickname, request.sender)
appendBuffer("[" .. OPERATOR .. "]: " .. request.nickname .. " is now offline")
elseif command == "metoo" then
putOnline(request.nickname, request.sender)
end
return
end

if request.target ~= nil then
appendBuffer(request.nickname .. " > " .. NICKNAME .. ": " .. request.message)
LAST_MSG_TARGET = request.nickname
else
appendBuffer(request.nickname .. ": " .. request.message)
end
end

--
function appendBuffer(message)
local length

length = message:len()

if length > WIDTH then
table.insert(BUFFER, message:sub(1, WIDTH))
POINTER = POINTER + 1
appendBuffer(message:sub(WIDTH + 1))
else
table.insert(BUFFER, message)
POINTER = POINTER + 1
end

SCROLL_POINTER = POINTER

showBuffer()
end

--
function showBuffer()
local i, line, bufferPointer, x, y, pointer

pointer = SCROLL_POINTER

if pointer == 0 then
return
elseif SCROLL_POINTER > POINTER then
SCROLL_POINTER = POINTER
pointer = POINTER
elseif POINTER < LINES + 1 then
SCROLL_POINTER = POINTER
pointer = POINTER
elseif POINTER > LINES and SCROLL_POINTER < LINES then
SCROLL_POINTER = LINES
pointer = SCROLL_POINTER
end

x, y = term.getCursorPos()

line = START_LINE

bufferPointer = -(LINES - 1 - pointer)

for i = bufferPointer, bufferPointer + (LINES - 1) do
term.setCursorPos(1, line)
term.clearLine()

if BUFFER[i] ~= nil then
term.write(tostring(BUFFER[i]))
end

line = line + 1
end

term.setCursorPos(x, y)
end

-- Fire up the application
main()