"""Buttons on Adafruit GPIO Expander Bonnet (MCP23017 device)."""
#
#   Original author: Mark Zvilius, 20-July-2019
#   For: The Sexton Corp
#
import board
import busio
import digitalio
from adafruit_mcp230xx.mcp23017 import MCP23017

import E2Configs


class BonnetButton:
    """A button provided by the Adafruit GPIO Expander Bonnet (MCP23017 device)."""
    #
    #   Polled and not debounced at present.
    #   TODO: Need an abstraction layer so different hardware can be substituted.
    #   TODO: Debounce buttons.
    #   TODO: Error handling improvements.
    #
    locn_to_pin = {
            'A0':0, 'A1':1, 'A2':2, 'A3':3, 'A4':4, 'A5':5, 'A6':6, 'A7':7, 
            'B0':8, 'B1':9, 'B2':10, 'B3':11, 'B4':12, 'B5':13, 'B6':14, 'B7':15 
            }

    mcp = None              # MCP23017 on the Bonnet
    button_lookup = None    # map from hardware location to command
    buttons = []            # the buttons


    def init_buttons(i2c_addr=0x20, button_lookup=None):
        """Initialize the buttons."""
        #
        #   This static function connects to the Adafruit Bonnet at the specified I2C
        #   address, and then instantiates BonnetButton objects using the button_lookup
        #   dictionary.
        #
        #   i2c_addr -- Address of MCP device on I2C bus.
        #   button_lookup -- Map from bonnet pin location to command.
        #
        #   Exception:  HardwareError -- failed to connect to MCP device
        #
        BonnetButton.button_lookup = button_lookup

        # Initialize the I2C bus.
        i2c = busio.I2C(board.SCL, board.SDA)

        # Create an instance of the device. It defaults to address 0x20 on the I2C bus.
        # If the MCP device is not found, the library throws ValueError.
        try:
            BonnetButton.mcp = MCP23017(i2c, i2c_addr)
        except ValueError:
            raise BonnetButton.HardwareError(
                    'Button Bonnet not found at 0x{0:x}.'.format(i2c_addr))

        # Create the buttons.
        for (locn,cmd) in button_lookup.items():
            name = E2Configs.button_map[locn]
            button = BonnetButton(locn, name, cmd)
            button.config()
            BonnetButton.buttons.append(button)


    def poll():
        """Poll for button presses, execute handler."""
        #
        # If Bonnet initialization failed, e.g. hardware does not exist, buttons[]
        # will be empty, so this will immediately exit as a no-op.
        #
        for b in BonnetButton.buttons:
            b.update()


    def __init__(self, locn, name, cmd):
        """Constructor."""
        #
        #   The "static" init_buttons() must be called to create the buttons.
        #   TODO: Make this "private" to enforce the above.
        #
        #   locn -- gpio pin on gpio expander board
        #   name -- name (human-readable function) of the button
        #   cmd -- command executed when button is pressed
        #
        self.locn = locn
        self.name = name
        self.command = cmd
        self.pin = BonnetButton.mcp.get_pin( BonnetButton.locn_to_pin[locn] )

    def config(self):
        """Configure the pin on the MCP device to be input and pull-up."""

        self.pin.direction = digitalio.Direction.INPUT
        self.pin.pull = digitalio.Pull.UP

        # Initialize the state variables.
        self.current = self.pin.value
        self.previous = self.current

    def update(self):
        """Keep track of button state. Call command handler when pressed."""

        self.previous = self.current
        self.current = self.pin.value
        pressed = (self.previous == 1) and (self.current == 0)
        if pressed:
            print('{0} pressed'.format(self.name))
            self.command()


    class Error(Exception):
        """Base class for exceptions in BonnetButton."""
        pass


    class HardwareError(Error):
        """Exception raised for hardware errors."""
        #
        #   Attributes:
        #   message -- human-readable description of error
        #

        def __init__(self, message):
            """Constructor."""
            self.message = message


