-- Copyright (c) 2016 John Schember <john@nachtimwald.com>
--
-- Permission is hereby granted, free of charge, to any person obtaining
-- a copy of this software and associated documentation files (the "Software"),
-- to deal in the Software without restriction, including without limitation
-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
-- and/or sell copies of the Software, and to permit persons to whom the
-- Software is furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-- DEALINGS IN THE SOFTWARE.

--- Fixed width unsigned integers backed by Lua's number type.
--
-- Maximum size is limited by the underlying number type size.
-- The detault size Lua uses is a 64 bit signed integer. Due to
-- overflow being used to handle wrapping nothing bigger than a
-- 32 bit unsigned integer can be supported/represented with this
-- backing type. In order to use larger than 32 bit unsigned integers
-- the BigNum backed uintb type needs to be used.
--
-- The size limitations Lua's number type imposes is why there is not
-- a public new function and instead a uintn must be created using 
-- one of the fixed size functions, u32, u16, and u8.
--
-- Also, while Lua's number type is a 64 bit signed type it is possible
-- to compile Lua with a smaller number size. If a smaller size is used
-- this could make u32 and friends stop functioning properly. However,
-- changing the number size is a conscious choice made at compile time.
-- As such, it is known to the programmer limitations that changing
-- the number size imposes.

local M = {}
local M_mt = {}

-- Private

--- Get the input in a workable form.
--
-- The order of input will not necessarily reflect the output. A swapped flag
-- will be returned to indicate that a, b are being returned b, a. The input
-- input is normalized into uint, number as the return values.
--
-- The unit returned is a new object and intended to be returned by the caller.
--
-- @param a Input.
-- @param b Input.
--
-- @return unit, number, swapped.
local function get_inputs(a, b)
    local swapped = false

    if not M.isuint(a) then
        a, b = b, a
        swapped = true
    end

    if M.isuint(b) then
        b = b._val
    else
        b = tonumber(b)
    end
   
    return a:copy(), b, swapped
end

local function reduce_range(o)
    if o._val == o._max then
        o._val = 0
    elseif o._val < 0 or o._val > o._max then
        o._val = o._val % o._max
    end
end

-- M_mt

M_mt.__index = M
M_mt.__add =
    function(a, b)
        local s

        a, b, s = get_inputs(a, b)

        if s then
            a._val = b + a._val
        else
            a._val = a._val + b
        end

        reduce_range(a)
        return a
    end
M_mt.__sub =
    function(a, b)
        local s

        a, b, s = get_inputs(a, b)

        if s then
            a._val = b - a._val
        else
            a._val = a._val - b
        end

        reduce_range(a)
        return a
    end
M_mt.__mul =
    function(a, b)
        a, b = get_inputs(a, b)

        a._val = a._val * b

        reduce_range(a)
        return a
    end
M_mt.__div =
    function(a, b)
        return a // b
    end
M_mt.__mod =
    function(a, b)
        local s

        a, b, s = get_inputs(a, b)

        if s then
            a._val = b % a._val
        else
            a._val = a._val % b
        end

        reduce_range(a)
        return a
    end
M_mt.__pow =
    function(a, b)
        local s

        a, b, s = get_inputs(a, b)

        if s then
            a._val = b ^ a._val
        else
            a._val = a._val ^ b
        end

        reduce_range(a)
        return a
    end
M_mt.__unm =
    function(a)
        a = a:copy()

        a._val = -a._val

        reduce_range(a)
        return a
    end
M_mt.__idiv =
    function(a, b)
        local s

        a, b, s = get_inputs(a, b)
        if s and b == 0 then
            a._val = 0
            return a
        end
        -- if b == 0 then divide by 0 exception, let it happen.

        if s then
            a._val = b // a._val
        else
            a._val = a._val // b
        end

        reduce_range(a)
        return a
    end
M_mt.__band =
    function(a, b)
        a, b = get_inputs(a, b)

        a._val = a._val & b

        reduce_range(a)
        return a
    end
M_mt.__bor =
    function(a, b)
        a, b = get_inputs(a, b)

        a._val = a._val | b

        reduce_range(a)
        return a
    end
M_mt.__bxor =
    function(a, b)
        a, b = get_inputs(a, b)

        a._val = a._val ~ b

        reduce_range(a)
        return a
    end
M_mt.__bnot =
    function(a)
        a = a:copy()

        a._val = ~a._val

        reduce_range(a)
        return a
    end
M_mt.__shl =
    function(a, b)
        local s

        a, b, s = get_inputs(a, b)

        if s then
            a._val = b << a._val
        else
            a._val = a._val << b
        end

        reduce_range(a)
        return a
    end
M_mt.__shr =
    function(a, b)
        local s

        a, b, s = get_inputs(a, b)

        if s then
            a._val = b >> a._val
        else
            a._val = a._val >> b
        end

        reduce_range(a)
        return a
    end
M_mt.__concat =
    function(a, b)
        if M.isuint(a) and M.isuint(b) then
            return a._val..b._val
        elseif M.isuint(a) and not M.isuint(b) then
            return a._val..b
        end
        return a..b._val
    end
M_mt.__len =
    function(a)
        return a._bits
    end
M_mt.__eq =
    function(a, b)
        a, b = get_inputs(a, b)
        return a._val == b
    end
M_mt.__lt =
    function(a, b)
        local s

        a, b, s = get_inputs(a, b)
        if s then
            return a._val > b
        end
        return a._val < b
    end
M_mt.__le =
    function(a, b)
        if a < b or a == b then
            return true
        end
        return false
    end
M_mt.__tostring = 
    function(a)
        return tostring(a._val)
    end

-- Private

local function new(bits, n)
    local o = setmetatable({}, M_mt)
    o._bits = bits
    o._max = 1 << o._bits

    if n == nil then
        n = 0
    end
    if M.isuint(n) then
        o._val = n._val
    else
        o._val = tonumber(n)
    end

    reduce_range(o)
    return o
end

-- Static

function M.isuint(t)
    if type(t) == "table" and getmetatable(t) == M_mt then
        return true
    end
    return false
end

function M.u8(v)
    return new(8, v)
end

function M.u16(v)
    return new(16, v)
end

function M.u32(v)
    return new(32, v)
end

-- M

function M:copy()
    return new(self._bits, self._val)
end

function M:set(n)
    if M.isuint(n) then
        self._val = n._val
    else
        self._val = tonumber(n)
    end
    reduce_range(self)
end

function M:swape()
    local v
    local n = 0
    local t

    v = self:asbytearray()
    for i=1,#v//2 do
        t = v[i]
        v[i] = v[#v-i+1]
        v[#v-i+1] = t
    end

    t = {}
    for i=#v,1,-1 do
        t[#t+1] = v[i]
    end

    for i=1,#t do
        n = n | (t[i] << i*8-8)
    end

    return new(self._bits, n)
end

function M:asnumber()
    return self._val
end

function M:ashex(width)
    local f = "%"
    if width ~= nil then
        f = f .. "0" .. tostring(width)
    end
    f = f .. "X"
    return f:format(self._val)
end

function M:asbytearray()
    local t = {}

    for i=self._bits-8,0,-8 do
        t[#t+1] = (self._val >> i) & 0xFF
    end

    return t
end

function M:asbytestring()
    local b

    b = self:asbytearray()
    for i=1,#b do
        b[i] = string.char(b[i])
    end
    return table.concat(b)
end

return M
