#!/usr/bin/lua
local VERSION = "0.0.1"
local rsmc = "/usr/bin/rsmc"

local rsmc_site_cfg = rsmc .. " GET: TAG_SITE_CFG"
local rsmc_port_cfg = rsmc .. " GET: TAG_PORT_CFG"
local rsmc_dev_cfg = rsmc .. " GET: TAG_DEV_CFG"

local sha1sum = "/bin/sha1sum"

local persistent_sha_file = "/var/rsm/dbdiff.sha1"

local ramdisk =  "/mnt/ramdisk"
-- Number of seconds to force a sync copy
local force_sync_timeout = 86400
local min_event_sync = 100
local min_trend_sync = 100

for iarg, varg in ipairs(arg) do
    if varg == '--help' then
        print("safe_the_sd Version: ", VERSION)
        print("--ramdisk <dir>\t", "Location of tmpfs from where the in memory database\n\t\t\tis kept.\n\t\t\tDefault: /mnt/ramdisk")
        print("--force-sync <seconds>", "Number of seconds to force sync from ramdisk.\n\t\t\tDefault: 86400 (1 day)")
        print("--trend-count <count>", "Maximum number of trends allowed before a sync is required.\n\t\t\tDefault: 100")
        print("--event-count <count>", "Maximum number of events allowed before a sync is required.\n\t\t\tDefault: 100")
        print("--help", "Print this help.")
        return
    elseif varg == '--ramdisk' then
        ramdisk = arg[iarg+1]
    elseif varg == '--force-sync' then
        force_sync_timeout = tonumber(arg[iarg+1])
    elseif varg == '--trend-count' then
        min_trend_sync = tonumber(arg[iarg+1])
    elseif varg == '--event-count' then
        min_event_sync = tonumber(arg[iarg+1])
    end
end

-- TODO(frans): Override the print to do nothing.
-- TODO: Uncomment this function if you want some output.
-- local print = function(...)
-- end

local rsmdb_tmpfs = ramdisk .."/rsmd.db"
local rsmdb_persist = "/var/rsm/rsmd.db"
local rsmdb_persist_tmp = rsmdb_persist .. ".tmp"

function split(c,str)
    a                       =       string.find(str, c)
    str             =       string.gsub(str, c, "", 1)
    aCount  =       0
    start   =       1
    array   =       {}
    last            =       0

    while a do
            array[aCount] = string.sub(str, start, a - 1)
            start = a
            a = string.find(str, c)

            str = string.gsub(str, c, "", 1)
            aCount = aCount + 1
    end

    return array
end

--Function to execute terminal commands to run a separate program and returns value as string
function ex(command)
    -- Starts program program in a separated process and returns a file handle
    -- that you can use to read data from this program (if mode is "r", the default)
    -- or to write data to this program (if mode is "w").
    local f = io.popen(command)

    -- Reads the file, according to the given formats, which specify what to read. For each format,
    -- the function returns a string (or a number) with the characters read, or nil if it cannot read
    -- data with the specified format.
    -- "*n" reads a number; this is the only format that returns a number instead of a string.
    -- "*a" reads the whole file, starting at the current position. On end of file, it returns the empty string
    -- "*|" reads the next line (skipping the end of line), returning nil on end of file. This is the default format.
    -- Number reads a string with up to this number of characters, returning nil on end of file.
    -- If number is zero, it reads nothing and returns an empty string, or nil on end of file.
    value = f:read("*a") or "none"

    -- Closes file. Note that files are automatically closed when their handles are garbage collected, but
    -- that takes an unpredictable amount of time to happen.
    f:close()

    return value
end

function get_controller_details()
    local retry = 3
    local details = nil
    repeat
        -- Read settings
        -- response = ex(rsmc .. " GET: TAG_SITE_CFG")
        response = ex(rsmc_site_cfg)
        local mac = response:match([[TAG_SITE_ID=%"([^%"]+)%"]]) or "unknown" 
        local name = response:match([[TAG_SITE_LABEL=%"([^%"]+)%"]]) or "unknown"
        if mac ~= "unknown" and name ~= "unknown" then 
            details = {}
            details['mac'] = mac
            details['name'] = name
            details['evt_cur_seq'] = tonumber(value:match([[TAG_SITE_CURR_EVT_SEQ=(%x+)]]), 16) or 0
            details['evt_last_seq'] = tonumber(value:match([[TAG_SITE_LAST_EVT_SEQ=(%x+)]]), 16) or 0
            details['evt_diff'] = details['evt_cur_seq'] - details['evt_last_seq']

            details['trend_cur_seq'] = tonumber(value:match([[TAG_SITE_CURR_TREND_SEQ=(%x+)]]), 16) or 0
            details['trend_last_seq'] = tonumber(value:match([[TAG_SITE_LAST_TREND_SEQ=(%x+)]]), 16) or 0 
            details['trend_diff'] = details['trend_cur_seq'] - details['trend_last_seq']

            retry = 0
        else
            retry = retry - 1
        end
    until (retry == 0)
    return details
end

--[[
    Try to open the file to see if it exists
]]
function file_exists(filename)
    local f = io.open(filename, "r")
    if f == nil then 
        return false
    else
        f:close()
    end
    return true
end

--[[
    Determine of the SHA1 stored matches the newly calculated
    SHA1.
    @sha_file: File used to read previous sync SHA1 value
    @sha1: Newly calculated SHA1 to be compared
]]
function diff_in_sha(sha_file, sha1)
    local diff_sha = true
    local f = io.open(sha_file, "r")
    if f == nil then 
        f = io.open(sha_file, "w+")
        f:write(sha1)
        f:close()
    else
        value = f:read("*a") or ""
        f:close()
        if sha1 == value then
            diff_sha = false
        else
            f = io.open(sha_file, "w+")
            f:write(sha1)
            f:close()
        end
    end
    return diff_sha
end

--[[
    Convert KMSG output string to list of KMsg objects.
    Built up according to the function specified in <code>parse_kobject</code>
    @arg[cmd]: Command to execute to produce port output.
    @arg[cfg_tag]: RSMC config tag to search for.
    @arg[parse_port]: Function that takes port details string and parses
    to a KMsg object.
]]
function parse_kobjects(cmd, cfg_tag, parse_kobject)
    local kobjects = {}
     -- Read settings
    -- response = ex(rsmc .. " GET: TAG_IP_CFG")
    response = ex(cmd)
    count = 0
    local idx_start, idx_end = response:find(cfg_tag, 0)
    -- No tags found so nor input kobjects
    if idx_start == nil or idx_end == nil then 
        return kobjects
    end
    done = false

    repeat
        local n_idx_start, n_idx_end = response:find(cfg_tag, idx_end)
        count = count + 1
        -- Parsed substring with port details
        local details = ''
        if n_idx_start ~= nil then
            details = response:sub((idx_end+1), (n_idx_start-1))            
            -- Update end index and move along reply
            idx_end = n_idx_end
        else
            -- Parse last port details
            details = response:sub((idx_end+1))            
            done = true
        end
        -- Add port to table
        local kobject = parse_kobject(details)        
        table.insert(kobjects, kobject)
    until done  
    return kobjects
end

--[[
    Perform copy of the database from tmpfs to the SD card.
    This is only done if the database exists in tmpfs.
]]
function perform_sync()
    print("Sync from ", rsmdb_tmpfs, " to ", rsmdb_persist_tmp)
    if (file_exists(rsmdb_tmpfs)) then
        -- Only if the file exist in /mnt/ramdisk do we make a backup
        local cmd_copy = 'cp -f ' .. rsmdb_tmpfs .. ' ' .. rsmdb_persist_tmp        
        ex(cmd_copy)
        
        local cmd_mv = 'mv -f ' .. rsmdb_persist_tmp .. ' ' .. rsmdb_persist        
        ex(cmd_mv)
        else 
            print("Ramdisk copy could not be found.")
    end
end

--[[
    Third and final check is to see if any config values have changed.
    NOTE: Currently we only check for device/port lables and device/port 
    operational status.
]]
function changes_detected()
    local sync_required = false    

    -- Parse device KObject details, label and operational status
    local function parse_device(value)
        local device = {}
        -- Device operational state
        device.state = value:match([[TAG_DEV_STATE=(%x+)]]) or "02"
        -- Device alarm status (Online/Offline)
        device.status = value:match([[TAG_DEV_STATUS=(%x+)]]) or "02"
        -- Device label
        device.label = value:match([[TAG_DEV_LABEL=%"(.-)%"]]) or "unknown"
        return device
    end
    
    -- Parse port KObject details, label and operational status
    local function parse_port_generic(value)
        local port = {}
        port.state = value:match([[TAG_PORT_STATE=(%x+)]]) or "02"
        port.label = value:match([[TAG_IO_LABEL=%"(.-)%"]]) or "unknown"
        return port
    end
    
    local sha = ''
    -- Retrieve and parse devices
    local devices = parse_kobjects(rsmc_dev_cfg, [[TAG_DEV_CFG]], parse_device)
    -- SHA1 the device label and operational status
    for di, dv in ipairs(devices) do
        sha = sha .. dv.label
        sha = sha .. dv.state
    end
    -- Retrieve and parse ports
    local ports = parse_kobjects(rsmc_port_cfg, [[TAG_PORT_CFG]], parse_port_generic)
    -- SHA1 the port label and operational status
    for pi, pv in ipairs(ports) do
        sha = sha .. pv.label
        sha = sha .. pv.state
        -- See if we have analogue details
    end
    -- Calculate SHA1 for above string    
    local cmd = 'echo "' .. sha .. '" | ' .. sha1sum
    local sha1_result = ex(cmd)
    local sha1 = split(" ", sha1_result)

    local stored_sha = sha1[0] or ''
    -- Get cached sha1
    
    sync_required = diff_in_sha(persistent_sha_file, stored_sha)
    return sync_required
end

local rsmdb_ts = ex("stat -c %Y " .. rsmdb_persist)
local current_ts = os.time()

local sync_required = false

-- If we could not read the timestamp or the value is more than
-- allowed force sync
if (rsmdb_ts == "" or tonumber(current_ts) - tonumber(rsmdb_ts) >= force_sync_timeout) then
    print("Force sync triggered.")
    sync_required = true
else
    local details = get_controller_details()
    -- If trends/event is more than allowed signal sync
    if details['evt_diff'] >= min_event_sync or details['trend_diff'] >= min_trend_sync then
        print("Event/trend triggered sync. ", details.name)
        sync_required = true
    else
        sync_required = changes_detected()
        if (sync_required) then
            print("Config change triggered sync. ", details.name)
        end
    end
end

if (sync_required) then 
    perform_sync()
end

-- print("Device count: ", #devices, " SHA ", sha)
-- local digital_inputs = parse_kobjects('cat di-cfg', [[TAG_IP_CFG]], parse_port_generic)
-- print("Digital input  count: ", #digital_inputs)
-- local digital_outputs = parse_kobjects('cat do-cfg', [[TAG_OP_CFG]], parse_port_generic)
-- print("Digital output count: ", #digital_outputs)
-- local analogue_inputs = parse_kobjects('cat ai-cfg', [[TAG_AI_CFG]], parse_port_generic)
-- print("Analogue inputs count: ", #analogue_inputs)
-- local analogue_outputs = parse_kobjects('cat ao-cfg', [[TAG_AO_CFG]], parse_port_generic)
-- print("Analogue outputs count: ", #analogue_outputs)



-- local digital_output_hash = get_digital_outputs()
-- -- Read syslog messages
-- local log_messages = ex("tail -n100 " .. messages_log_file)
-- local log_lines = split('\n', log_messages)

-- -- Try to find some exceptions
-- -- for exception in log_messages:gmatch(exceptions[1].regex) do
-- --     print(exception)
-- -- end
-- for k,v in ipairs(exceptions) do

--     for lk, lv in ipairs(log_lines) do 
--         local regex_match = lv:match(v.regex) or nil
--         if regex_match ~= nil then
--             print(regex_match)
--             send_message({mac=mac_address, site=site_label, message=regex_match, level=v.level})
--             break
--         end     
--     end
   
-- end