--[[
	This Lua module provides access to the MCP23017 module.

    The MCP23017 is a port expander and provides 16 channels for inputs and outputs.
    Up to 8 devices (128 channels) are possible by the configurable address (A0 - A2 Pins).

    Due to the 16 channels, 2 bytes are required for switching outputs or reading input signals. These are A and B.
    A single pin can be set or a whole byte.

    The numbering of the individual pins starts at 0 and ends with 7.
    The numbers are for each register GPIO A and GPIO B.

	The module requires `i2c` and `bit` C module built into firmware.

	@author Marcel P. | Plomi.net
	@github https://github.com/plomi-net
	@version 1.0.0
]]

local i2c, string, issetBit, setBit, clearBit, error =
      i2c, string, bit.isset, bit.set, bit.clear, error
local isINPUT, isGPB, isHIGH = true, true, true

-- registers (not used registers are commented out)
local MCP23017_IODIRA = 0x00
local MCP23017_IODIRB = 0x01
local MCP23017_DEFVALA = 0x06
local MCP23017_DEFVALB = 0x07
local MCP23017_GPIOA = 0x12
local MCP23017_GPIOB = 0x13
--[[
local MCP23017_IPOLA = 0x02
local MCP23017_IPOLB = 0x03
local MCP23017_GPINTENA = 0x04
local MCP23017_GPINTENB = 0x05
local MCP23017_DEFVALA = 0x06
local MCP23017_DEFVALB = 0x07
local MCP23017_INTCONA = 0x08
local MCP23017_INTCONB = 0x09
local MCP23017_IOCON = 0x0A
local MCP23017_IOCON2 = 0x0B
local MCP23017_GPPUA = 0x0C
local MCP23017_GPPUB = 0x0D
local MCP23017_INTFA = 0x0E
local MCP23017_INTFB = 0x0F
local MCP23017_INTCAPA = 0x10
local MCP23017_INTCAPB = 0x11
local MCP23017_OLATA = 0x14
local MCP23017_OLATB = 0x15
]]

-- metatable
local mcp23017 = {
    INPUT = isINPUT,
    OUTPUT = not isINPUT,
    GPA = not isGPB,
    GPB = isGPB,
    HIGH = isHIGH,
    LOW = not isHIGH
}
mcp23017.__index = mcp23017

-- check device is available on address
local function checkDevice(address, i2cId)
    i2c.start(i2cId)
    local response = i2c.address(i2cId, address, i2c.TRANSMITTER)
    i2c.stop(i2cId)
    return response
end

-- write byte
local function writeByte(address, i2cId, registerAddr, val)
    i2c.start(i2cId)
    i2c.address(i2cId, address, i2c.TRANSMITTER)
    i2c.write(i2cId, registerAddr)
    i2c.write(i2cId, val)
    i2c.stop(i2cId)
end

-- read byte
local function readByte(address, i2cId, registerAddr)
    i2c.start(i2cId)
    i2c.address(i2cId, address, i2c.TRANSMITTER)
    i2c.write(i2cId, registerAddr)
    i2c.stop(i2cId)
    i2c.start(i2cId)
    i2c.address(i2cId, address, i2c.RECEIVER)
    local data = i2c.read(i2cId, 1)
    i2c.stop(i2cId)
    return string.byte(data)
end

-- check pin is in range
local function checkPinIsInRange(pin)
    if pin > 7 or pin < 0 then
        error("MCP23017 the pin must be between 0 and 7")
    end
    return pin
end

local function reset(address, i2cId)
    writeByte(address, i2cId, MCP23017_IODIRA, 0xFF)
    writeByte(address, i2cId, MCP23017_IODIRB, 0xFF)
end

-- setup device
local function setup(address, i2cId)

    -- check device address (0x20 to 0x27)
    if (address < 32 or address > 39) then
        error("MCP23017 address is out of range")
    end

    if (checkDevice(address, i2cId) ~= true) then
        error("MCP23017 device on " .. string.format('0x%02X', address) .. " not found")
    else
        reset(address, i2cId)
        return 1
    end
end

return function(address, i2cId)
    local self = setmetatable({}, mcp23017)

    if setup(address, i2cId) then
        self.writeIODIR = function(sf, bReg, newByte) -- luacheck: no unused
            writeByte(address, i2cId,
                bReg == isGPB and MCP23017_IODIRB or MCP23017_IODIRA,
                newByte)
        end

        self.writeGPIO = function(sf, bReg, newByte) -- luacheck: no unused
            writeByte(address, i2cId,
                bReg == isGPB and MCP23017_GPIOB or MCP23017_GPIOA, newByte)
        end

        self.readGPIO = function(sf, bReg) -- luacheck: no unused
            return readByte(address, i2cId, -- upvals
                bReg == isGPB and MCP23017_GPIOB or MCP23017_GPIOA)
        end

        -- read pin input
        self.getPinState = function(sf, bReg, pin) -- luacheck: no unused
            return issetBit(readByte(address, i2cId,
                bReg == isGPB and MCP23017_GPIOB or MCP23017_GPIOA),
                checkPinIsInRange(pin))
        end

        -- set pin to low or high
        self.setPin = function(sf, bReg, pin, state) -- luacheck: no unused
            local inReq = bReg == isGPB and MCP23017_GPIOB or MCP23017_GPIOA
            local inPin = checkPinIsInRange(pin)
            local response = readByte(address, i2cId, inReq)
            writeByte(address, i2cId, inReq,
                state == isHIGH and setBit(response, inPin) or clearBit(response, inPin))
            return true
        end

        -- set mode for a pin
        self.setMode = function(sf, bReg, pin, mode) -- luacheck: no unused
            local inReq = bReg == isGPB and MCP23017_IODIRB or MCP23017_IODIRA
            local inPin = checkPinIsInRange(pin)
            local response = readByte(address, i2cId, inReq)
            writeByte(address, i2cId, inReq,
                mode == isINPUT and setBit(response, inPin) or clearBit(response, inPin))
            return true
        end

        -- reset gpio mode
        self.reset = function(sf) -- luacheck: no unused
            reset(address, i2cId)
        end

        -- setup internal pullup
        self.setInternalPullUp = function(sf, bReg, iByte) -- luacheck: no unused
            writeByte(address, i2cId,
                bReg == isGPB and MCP23017_DEFVALB or MCP23017_DEFVALA, iByte)
        end

        return self
    end
    return nil
end


