-- Luafetch
-- Created by: Phate6660

-- Runs an external command in bash and returns the output.
local function cmd(command)
    local handle = io.popen(command)
    local result = handle:read("*a")
    handle:close()
    return result
end

-- Reads an environmental variable and returns the contents if possible,
-- otherwise it returns a dynamic error message stating which variable failed.
local function env(var)
    local data = os.getenv(var)
    if not data then
        return 'N/A (could not read "$' .. var .. '", are you sure it is set?)'
    else
        return data
    end
end

-- Returns a bool based on whether or not the file exists.
local function file_exists(name)
    local f=io.open(name,"r")
    if f~=nil then 
        io.close(f)
        return true
    else
        return false
    end
end

-- Takes a string, creates a table delimited by newlines,
-- counts the number of elements, then returns the number.
local function linecount(string)
    local lines = {}
    for line in string.gmatch(string, '([^\n]+)') do
        table.insert(lines, line)
    end
    local count = {}
    for i, _ in ipairs(lines) do
        table.insert(count, i)
    end
    return count[#count+1-1]
end

-- An Android specific function for matching the processor ID (obtained from `/proc/cpuinfo`),
-- with the processor name and returns said name.
local function match_processor(id)
    local hardware = {
        ['0xd46'] = "Cortex-A510",
    }
    return hardware[id]
end

-- Takes a string, the character to find, and what to replace it with.
-- Returns the string with all instances of the character replaced.
local function replace(arg, char, rep)
    if string.match(arg, char) then
        return arg:gsub(char, rep)
    else
        return arg -- Just return arg without doing anything if char wasn't found.
    end
end

-- A function to return output, and optionally strip newlines from it.
local function return_output(output, strip)
    strip = strip or false
    if strip == true then
        return replace(output, '\n', '')
    else
        return output
    end
end

-- Takes a file path, optionally a line number, and optionally to strip newlines.
-- With just the file path, it'll return the contents of the file.
-- Specifying a line number will return only that line.
local function read(file_path, line_number, strip)
    line_number = line_number or 'nil'
    strip = strip or false
    local file = io.open(file_path, 'r')
    if not file then
        return 'N/A (could not read "' .. file_path .. '")'
    else
        local contents = file:read '*a'
        file:close()
        if line_number == 'nil' then
            return return_output(contents, strip)
        else
            local contents_table = {}
            local delim = '\n'
            for line in string.gmatch(contents, '([^' .. delim .. ']+)') do
                table.insert(contents_table, line)
            end
            return return_output(contents_table[line_number], strip)
        end
    end
end

-- Split a string based on a delimiter, return a table.
local function split(string, delim)
    local string_table = {}
    for entry in string.gmatch(string, '([^' .. delim .. ']+)') do
        table.insert(string_table, entry)
    end
    return string_table
end

-- Checks if the OS is Android, currently by parsing the kernel
-- name and checking if it contains 'android', since it seems that
-- most of not all android versions will contain 'android'
-- in the kernel version.
local function android()
    local s = return_output(cmd('uname -r'))
    if string.match(s, "android") then
        return true
    else
        return false
    end
end

local function return_cpu()
    if android() then
        line = read('/proc/cpuinfo', 7, true)
    else
        line = read('/proc/cpuinfo', 5, true)
    end
    local line_table = split(line, ':')
    local result = line_table[2]:sub(2) -- Remove leading space from using ':' as delimiter.
    if android() then
      return match_processor(result)
    else
      return result
    end
end

local function return_distro()
    local line = read('/etc/os-release', 3, true)
    if android() then
        local av = cmd("getprop ro.build.version.release")
        av = return_output(av, true)
        local android = "Android " .. av
        local kv = cmd("uname -r")
        kv = return_output(kv, true)
        local device = cmd("getprop ro.vendor.product.display")
        device = return_output(device, true)
        return android, kv, device
    end
    local line_table = split(line, '=')
    return replace(line_table[2], '"', '')
end

-- TODO: Gather more info, such as total and used memory.
local function return_memory()
    local line = read('/proc/meminfo', 1, true)
    local line_table = split(line, ' ')
    local kb = tonumber(line_table[2])
    if kb > 1024 then
        local mb = kb / 1024
        local mb_table = split(tostring(mb), '.')
        return mb_table[1] .. ' MB'
    end
    return kb
end

local function return_packages(mngr)
    mngr = mngr or 'nil'
    if mngr == "portage" then
        -- '/var/db/pkg/*/*' is a list of all packages.
        -- TODO: Get the list of dirs in pure lua.
        local dirs = cmd('find "/var/db/pkg/" -mindepth 2 -maxdepth 2 -type d -printf "%f\n"')
        local dirs_list = dirs:read('*a')
        dirs:close()
        local total = linecount(dirs_list)
        local explicit_list = read('/var/lib/portage/world', nil, false)
        local explicit = linecount(explicit_list)
        return explicit .. ' (explicit), ' .. total .. ' (total) ' .. '| Portage'
    elseif mngr == "pacman" then
        local total = cmd('pacman -Qq | wc -l')
        return replace(total .. ' (total) | Pacman', '\n', '')
    -- `pkg` is also a BSD package manager, but we'll get to that later
    -- if this reaches a stage where it expands to BSD.
    -- For now, this is in reference to the `pkg`
    -- package manager built in to Termux
    elseif mngr == 'pkg' or 'apt' or 'dpkg' then
        local output = cmd("dpkg -l --no-pager")
        output = linecount(output) - 4
        return output .. " (total) | " .. mngr
    elseif mngr == 'nil' then
        return 'N/A (no package manager was passed to the function)'
    else
        return 'N/A (' .. mngr .. ' is unsupported right now)'
    end
end

local function return_music(player)
    player = player or 'nil'
    if player == 'mpd' then
        local line = cmd('mpc -f "%artist% - %album% - %title%" | head -n1')
        return replace(usable_line, '\n', '')
    elseif player == 'spotify' then
        local line = cmd('playerctl -p spotify metadata -f "{{ artist }} - {{ album }} - {{ title }}"')
        return replace(usable_line, '\n', '')
    elseif player == 'nil' then
        return 'N/A (no player selected)'
    else
        return 'N/A (' .. player .. ' is unsupported right now)'
    end
end

local function return_uptime()
    local uptime = tonumber(split(read('/proc/uptime'), '.')[1])
    if android() then
        -- TODO: Find a better method than running a command,
        -- though unfortunately `/proc/uptime` is either missing or inaccessible on Android.
        local output = cmd("uptime -p")
        local usable_output = return_output(output, true)
        -- TODO: Clean up and format output before returning it.
        return usable_output
    end
    if uptime > 86400 then
        local days_pre = uptime / 60 / 60 / 24
        days_pre = split(tostring(days_pre), '.')[1]
        Days = days_pre .. 'd'
    else
        Days = ''
    end
    if uptime > 3600 then
        local hours_pre = (uptime / 60 / 60) % 24
        hours_pre = split(tostring(hours_pre), '.')[1]
        Hours = hours_pre .. 'h'
    else
        Hours = ''
    end
    if uptime > 60 then
        local minutes_pre = (uptime / 60) % 60
        minutes_pre = split(tostring(minutes_pre), '.')[1]
        Minutes = minutes_pre .. 'm'
    else
        Minutes =  ''
    end
    if Days == '' then
        return (Days .. ' ' .. Hours .. ' ' .. Minutes):sub(2)
    else
        return Days .. ' ' .. Hours .. ' ' .. Minutes
    end
end

-- Gather Information
-- Notes:
-- * the first arg passed to the script is the packaga manager
-- * the second arg the music player if music info is wanted
-- TODO: hide info behind options, this will require more robust arg parsing.
local cpu               = return_cpu()
if android() then
    distro, kernel, device = return_distro()
else
    device              = read(
        '/sys/devices/virtual/dmi/id/product_name', 
        nil, true
    )
    distro              = return_distro()
    kernel              = read('/proc/sys/kernel/osrelease', nil, true)
end
local editor            = env('EDITOR')
local hostname          = read('/etc/hostname', nil, true)
local memory            = return_memory()
local packages          = return_packages(arg[1])
local shell             = env('SHELL')
local uptime            = return_uptime()
local user              = env('USER')
local music             = return_music(arg[2])

print('cpu      =  ' .. cpu      .. '\n'
   .. 'device   =  ' .. device   .. '\n'
   .. 'distro   =  ' .. distro   .. '\n'
   .. 'editor   =  ' .. editor   .. '\n'
   .. 'hostname =  ' .. hostname .. '\n'
   .. 'kernel   =  ' .. kernel   .. '\n'
   .. 'memory   =  ' .. memory   .. '\n'
   .. 'packages =  ' .. packages .. '\n'
   .. 'shell    =  ' .. shell    .. '\n'
   .. 'uptime   =  ' .. uptime   .. '\n'
   .. 'user     =  ' .. user     .. '\n'
   .. 'music    =  ' .. music
)