local utils = require('utils')

local M = {}

-- type functions

function M._sequential_Q(obj)
    return M._list_Q(obj) or M._vector_Q(obj)
end

function M._equal_Q(a,b)
    if M._symbol_Q(a) and M._symbol_Q(b) then
        return a.val == b.val
    elseif M._sequential_Q(a) and M._sequential_Q(b) then
        if #a ~= #b then return false end
        for i, v in ipairs(a) do
            if not M._equal_Q(v,b[i]) then return false end
        end
        return true
    elseif M._hash_map_Q(a) and M._hash_map_Q(b) then
        if #a ~= #b then return false end
        for k, v in pairs(a) do
            if not M._equal_Q(v,b[k]) then return false end
        end
        return true
    else
        return a == b
    end
end

function M.copy(obj)
    if type(obj) == "function" then
        return M.FunctionRef:new(obj)
    end
    if type(obj) ~= "table" then return obj end

    -- copy object data
    local new_obj = {}
    for k,v in pairs(obj) do
        new_obj[k] = v
    end

    -- copy metatable and link to original
    local old_mt = getmetatable(obj)
    if old_mt ~= nil then
        local new_mt = {}
        for k,v in pairs(old_mt) do
            new_mt[k] = v
        end
        setmetatable(new_mt, old_mt)
        setmetatable(new_obj, new_mt)
    end

    return new_obj
end

function M.slice(lst, start, last)
    if last == nil then last = #lst end
    local new_lst = {}
    if start <= last then
        for i = start, last do
            new_lst[#new_lst+1] = lst[i]
        end
    end
    return new_lst
end

-- Error/exceptions

M.MalException = {}
function M.MalException:new(val)
    local newObj = {val = val}
    self.__index = self
    return setmetatable(newObj, self)
end
function M._malexception_Q(obj)
    return utils.instanceOf(obj, M.MalException)
end

function M.throw(val)
    error(M.MalException:new(val))
end

-- Nil

local NilType = {}
function NilType:new(val)
    local newObj = {}
    self.__index = self
    return setmetatable(newObj, self)
end
M.Nil = NilType:new()
function M._nil_Q(obj)
    return obj == Nil
end

-- Numbers
function M._number_Q(obj)
    return type(obj) == "number"
end

-- Strings
function M._string_Q(obj)
    return type(obj) == "string" and "\u{029e}" ~= string.sub(obj,1,2)
end

-- Symbols

M.Symbol = {}
function M.Symbol:new(val)
    local newObj = {val = val}
    self.__index = self
    return setmetatable(newObj, self)
end
function M._symbol_Q(obj)
    return utils.instanceOf(obj, M.Symbol)
end

-- Keywords
-- 5.1 does not support unicode escapes.  Their length vary between 5.3 and 5.4.
_keyword_mark = "\u{029e}"  -- Two bytes.
_keyword_mark_len = string.len(_keyword_mark)

function M._keyword_Q(obj)
    return type(obj) == "string" and _keyword_mark == string.sub(obj,1,_keyword_mark_len)
end

function M._keyword_from_lua_string(value)
    return _keyword_mark .. value
end

function M._lua_string_from_keyword(obj)
    return string.sub(obj, _keyword_mark_len + 1)
end


-- Lists

M.List = {}
function M.List:new(lst)
    local newObj = lst and lst or {}
    self.__index = self
    return setmetatable(newObj, self)
end
function M._list_Q(obj)
    return utils.instanceOf(obj, M.List)
end
function M.List:slice(start,last)
    return M.List:new(M.slice(self,start,last))
end

-- Vectors

M.Vector = {}
function M.Vector:new(lst)
    local newObj = lst and lst or {}
    self.__index = self
    return setmetatable(newObj, self)
end
function M._vector_Q(obj)
    return utils.instanceOf(obj, M.Vector)
end
function M.Vector:slice(start,last)
    return M.Vector:new(M.slice(self,start,last))
end

-- Hash Maps
--
M.HashMap = {}
function M.HashMap:new(val)
    local newObj = val and val or {}
    self.__index = self
    return setmetatable(newObj, self)
end
function M.hash_map(...)
    return M._assoc_BANG(M.HashMap:new(), ...)
end
function M._hash_map_Q(obj)
    return utils.instanceOf(obj, M.HashMap)
end
function M._assoc_BANG(hm, ...)
    local arg = table.pack(...)
    for i = 1, #arg, 2 do
        hm[arg[i]] = arg[i+1]
    end
    return hm
end
function M._dissoc_BANG(hm, ...)
    local arg = table.pack(...)
    for i = 1, #arg do
        hm[arg[i]] = nil
    end
    return hm
end

-- Functions

M.MalFunc = {}
function M.MalFunc:new(fn, ast, env, params)
    local newObj = {fn = fn, ast = ast, env = env,
                    params = params, ismacro = false}
    self.__index = self
    return setmetatable(newObj, self)
end
function M._malfunc_Q(obj)
    return utils.instanceOf(obj, M.MalFunc)
end
function M._fn_Q(obj)
    return type(obj) == "function" or (M._malfunc_Q(obj) and not obj.ismacro)
end
function M._macro_Q(obj)
    return M._malfunc_Q(obj) and obj.ismacro
end

-- Atoms

M.Atom = {}
function M.Atom:new(val)
    local newObj = {val = val}
    self.__index = self
    return setmetatable(newObj, self)
end
function M._atom_Q(obj)
    return utils.instanceOf(obj, M.Atom)
end

-- FunctionRefs

M.FunctionRef = {}
function M.FunctionRef:new(fn)
    local newObj = {fn = fn}
    return setmetatable(newObj, self)
end
function M._functionref_Q(obj)
    return utils.instanceOf(obj, M.FunctionRef)
end
function M.FunctionRef:__call(...)
    return self.fn(...)
end

return M
