/**
 * @file pcal6408a_gpio_expander.c
 *
 * @brief This file contains functions that drives the PCAL6408A GPIO expander
 *
 * @copyright Copyright 2024 Antaris, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "pcal6408a_gpio_expander.h"
#include "main.h"

pcal6408a_dev hgpio_exp_ps_u13;
extern I2C_HandleTypeDef hi2c1;

/* Static function declarations */

/*!
 * @brief This utility API is used to set or reset a bit in the data
 * @param[in] data  : Data whose bit needs to be set or reset
 * @param[in] pin   : bit position.
 * @param[in] flag  : set or reset
 * @return data after the required bit is set or reset
 * @retval data after the required bit is set or reset
 */
static uint8_t bit_write (uint8_t data, pcal6408a_gpio_pin pin, uint8_t flag);

/**
 * @brief Writes a value to a specific bit in a byte of data.
 *
 * This function modifies a specific bit within a byte of data based on the given pin and flag. It is typically used to update the state of a particular GPIO pin represented by `pin` within a byte of data.
 *
 * @param[in] data The original byte of data where the bit will be modified.
 * @param[in] pin The GPIO pin number that indicates which bit to modify. This should be a value corresponding to the bit position within the byte (0-7).
 * @param[in] flag The value to write to the specified bit. It should be either 0 or 1. If `flag` is non-zero, the bit is set; otherwise, it is cleared.
 *
 * @return uint8_t The modified byte of data with the specified bit updated.
 */
static uint8_t bit_write (uint8_t data, pcal6408a_gpio_pin pin, uint8_t flag);

/**
 * @brief Performs a self-test on a specific GPIO pin of the GPIO expander.
 *
 * This function is used to execute a self-test routine for a given GPIO pin on the GPIO expander. The self-test checks if the specified GPIO pin is functioning correctly. The result of the self-test is indicated by the return value.
 *
 * @param[in] pin The GPIO pin to be tested. This should be a value that corresponds to the pin number on the GPIO expander.
 *
 * @return int8_t
 * - `0` if the self-test is successful and the GPIO pin is functioning correctly.
 * - Non-zero error code if the self-test fails or the GPIO pin is not functioning correctly.
 */
int8_t gpio_exp_self_test(pcal6408a_gpio_pin pin);

/*!
 * @brief This utility API is used to check the validity of the gpio pin number
 * @param[in] pin  : pin number that needs to validated.
 * @return Result of pin number validation
 * @retval PCAL6408A_OK -> Success / -PCAL6408A_E_OUT_OF_RANGE -> Error.
 */
static int8_t is_gpio_valid (pcal6408a_gpio_pin pin);

/*********************** User function definitions ****************************/

/**
 * @brief This API initializes the gpio pin as per the gpio_config. This api
 * needs to be called for all the gpio expander pins.
 */

uint8_t pcal6408a_i2c_write(void* intf_hdl, uint16_t slv_addr, uint8_t reg_addr, uint8_t *data, uint16_t len)
{
    I2C_HandleTypeDef *hi2c = (I2C_HandleTypeDef*)intf_hdl;
    int8_t sts = PCAL6408A_E_NULL_PTR;
    uint8_t data_array[2] = {0};
    data_array[0]=reg_addr;
    data_array[1]=data[0];

    if(HAL_I2C_Master_Transmit(hi2c,slv_addr,data_array,2,500) == HAL_OK)
    {
        sts=PCAL6408A_OK;
    }
    return sts;
}

/**
 * @brief  This API is used for i2c read.
 */
uint8_t pcal6408a_i2c_read(void* intf_hdl, uint16_t slv_addr, uint8_t reg_addr, uint8_t *data, uint16_t len)
{
    I2C_HandleTypeDef *hi2c = (I2C_HandleTypeDef*)intf_hdl;
    int8_t sts = PCAL6408A_E_NULL_PTR;
	if(HAL_I2C_Master_Transmit(hi2c, (uint16_t)(slv_addr), &reg_addr, 1, 500) == HAL_OK)
    {
		if(HAL_I2C_Master_Receive(hi2c, (uint16_t)(slv_addr), data, len, 500) == HAL_OK)
        {
            sts =PCAL6408A_OK;
        }
        else
        {
            sts =PCAL6408A_E_COM_FAIL;
        }
    }
    else
    {
        sts =PCAL6408A_E_COM_FAIL;
    }
    return sts;
}

/**
 * @brief  This API is used for init gpio expander.
 */
void ps_brd_gpio_exp_test_init(uint8_t ahw_id)
{
	switch(ahw_id)
	{
	case GPIO_EXPANDER_PCAL6408A_PS:
	{
		hgpio_exp_ps_u13.io_intf_hdle = &hi2c1;
		hgpio_exp_ps_u13.slave_addr = 0x42;
		hgpio_exp_ps_u13.write = pcal6408a_i2c_write;
		hgpio_exp_ps_u13.read = pcal6408a_i2c_read;
	}
		break;
	default:
		break;
	}
}

/**
 * @brief  This API is used for init pcal6408.
 */
int8_t pcal6408a_gpio_init(pcal6408a_dev *hdev)
{
    int8_t sts = PCAL6408A_E_NULL_PTR;

    if(hdev == NULL)
    {
        sts = PCAL6408A_E_NULL_PTR;
    }
    else
    {
        sts=PCAL6408A_OK;
    }
    return sts;
}

/**
 * @brief This API is used to read the pcal6408a registers and writes data to it ,to validate GPIO expender
 */
int8_t pcal6408a_self_test(pcal6408a_dev *hdev)
{
    int8_t sts = PCAL6408A_E_COM_FAIL;
    uint8_t data = 0;
    uint8_t write_data =0x41;
    uint8_t read_data =0;
    sts = pcal6408a_read(hdev,PCAL6408_REG_INPUT_LATCH, &data,2);
    if(sts ==PCAL6408A_OK)
    {
        pcal6408a_write(hdev,PCAL6408_REG_INPUT_LATCH ,&write_data, 1);
        pcal6408a_read(hdev, PCAL6408_REG_INPUT_LATCH, &read_data, 1);
    }
    if(write_data==read_data)
    {
        sts= PCAL6408A_OK;
    }
    else
    {
        sts=PCAL6408A_E_COM_FAIL;
    }
    pcal6408a_write(hdev, PCAL6408_REG_INPUT_LATCH, &data, 1);
    return sts ;
}

/**
 * @brief This API is used to read the pcal6408a registers through i2c interface
 */
int8_t pcal6408a_read(pcal6408a_dev *hdev, uint8_t reg, uint8_t *data, uint16_t len)
{
    int8_t sts = PCAL6408A_E_NULL_PTR;

    if ((hdev == NULL) || (hdev->read == NULL))
    {
        sts = PCAL6408A_E_NULL_PTR;
    }
    else if (len == 0)
    {
        sts = PCAL6408A_E_READ_WRITE_LENGTH_INVALID;
    }
    else
    {

        sts = hdev->read(hdev->io_intf_hdle, hdev->slave_addr, reg, data, len);
    }

    return sts;
}

/**
 * @brief This API is used to write the pcal6408a registers through i2c
 * interface
 */
int8_t pcal6408a_write(pcal6408a_dev *hdev, uint8_t reg, uint8_t *data, uint16_t len)
{
    int8_t sts = PCAL6408A_E_NULL_PTR;

    if ((hdev == NULL) || (hdev->read == NULL))
    {
        sts = PCAL6408A_E_NULL_PTR;
    }
    else if (len == 0)
    {
        sts = PCAL6408A_E_READ_WRITE_LENGTH_INVALID;
    }
    else
    {
        sts = hdev->write(hdev->io_intf_hdle, hdev->slave_addr, reg, data, len);
    }

    return sts;
}

/**
 * @brief This API is used to read the state of the gpio pin
 */
int8_t pcal6408a_readpin(pcal6408a_dev *hdev, pcal6408a_gpio_pin pin, pcal6408a_gpio_pinstate *state)
{
    uint8_t data = 0;
    int8_t sts = PCAL6408A_E_NULL_PTR;
    sts = is_gpio_valid(pin);
    if (PCAL6408A_OK != sts)
    {
        sts = PCAL6408A_E_OUT_OF_RANGE;
    }
    else
    {
        if(pcal6408a_get_input_latch(hdev, pin)== LATCH)
        {
            pcal6408a_set_input_latch(hdev, pin, NO_LATCH);
            sts = pcal6408a_read(hdev, PCAL6408_REG_INPUT, &data, 1);
            pcal6408a_set_input_latch(hdev, pin, LATCH);
        }
        if (PCAL6408A_OK == sts)
        {
            data = (data & pin);
            if(data>0)
            {
                *state  = PIN_SET;
            }
            else
            {
                *state  = PIN_RESET;
            }
        }
        else
        {

        }
    }
    return sts;
}

/**
 * @brief This API is used to write the state of the gpio pin
 */
int8_t pcal6408a_writepin(pcal6408a_dev *hdev, pcal6408a_gpio_pin pin, pcal6408a_gpio_pinstate state)
{
    uint8_t data = 0;
    int8_t sts = PCAL6408A_E_NULL_PTR;
    sts = is_gpio_valid(pin);
    if (PCAL6408A_OK != sts)
    {
        sts = PCAL6408A_E_OUT_OF_RANGE;
    }
    else
    {
        sts = pcal6408a_read(hdev, PCAL6408_REG_OUTPUT, &data, 1);
        if (PCAL6408A_OK == sts)
        {
            data = bit_write(data, pin, state);
            sts = pcal6408a_write(hdev, PCAL6408_REG_OUTPUT, &data, 1);
        }
        else
        {

        }
    }
    return sts;
}

/**
 * @brief This API is used to toggle the state of the gpio pin
 */
int8_t pcal6408a_togglepin(pcal6408a_dev *hdev, pcal6408a_gpio_pin pin)
{
    uint8_t data = 0;
    int8_t sts = PCAL6408A_E_NULL_PTR;
    sts = is_gpio_valid(pin);
    if (PCAL6408A_OK != sts)
    {
        sts = PCAL6408A_E_OUT_OF_RANGE;
    }
    else
    {
        sts = pcal6408a_read(hdev, PCAL6408_REG_OUTPUT, &data, 1);
        if (PCAL6408A_OK == sts)
        {
            data = (data ^ (1 << pin));
            sts = pcal6408a_write(hdev, PCAL6408_REG_OUTPUT, &data, 1);
        }
        else
        {

        }
    }
    return sts;
}

/**
 * @brief This API configures the gpio pin to be either input (dir = 0) or
 * output (dir = 1)
 */
int8_t pcal6408a_set_dir(pcal6408a_dev *hdev, pcal6408a_gpio_pin pin, pcal6408a_pin_dir dir)
{
    uint8_t data = 0;
    int8_t sts = PCAL6408A_E_NULL_PTR;
    sts = is_gpio_valid(pin);
    if (PCAL6408A_OK != sts)
    {
        sts = PCAL6408A_E_OUT_OF_RANGE;
    }
    else
    {
        sts = pcal6408a_read(hdev, PCAL6408_REG_CONFIG, &data, 1);
        if (PCAL6408A_OK == sts)
        {
            data = bit_write(data, pin, dir);
            sts = pcal6408a_write(hdev, PCAL6408_REG_CONFIG, &data, 1);
        }
        else
        {

        }
    }
    return sts;
}

/**
 * @brief This API configures the output drive level of the GPIO pin.
 */
int8_t pcal6408a_set_strength(pcal6408a_dev *hdev, pcal6408a_gpio_pin pin, pcal6408a_pin_strgth strength)
{
    uint8_t data= 0;
    int8_t sts = PCAL6408A_E_NULL_PTR;
    uint8_t mask = PCAL6408_REG_OUT_STRENGTH_MASK;

    sts = is_gpio_valid(pin);
    if (PCAL6408A_OK != sts)
    {
        sts = PCAL6408A_E_OUT_OF_RANGE;
    }
    else
    {
        if(pin > PCAL6408A_GPIO_3)
        {
            sts = pcal6408a_read(hdev, PCAL6408_REG_OUT_STRENGTH1, &data, 1);
        }
        else
        {
            sts = pcal6408a_read(hdev, PCAL6408_REG_OUT_STRENGTH0, &data, 1);
        }

        if (PCAL6408A_OK == sts)
        {
            if(pin > PCAL6408A_GPIO_3)
            {
                mask = (mask << ((pin-4)*2));
                data = ((data & ~mask) | (strength << ((pin-4)*2)));
                sts = pcal6408a_write(hdev, PCAL6408_REG_OUT_STRENGTH1, &data, 1);
            }
            else
            {
                mask = (mask << (pin*2));
                data = ((data & ~mask) | (strength << (pin*2)));
                sts = pcal6408a_write(hdev, PCAL6408_REG_OUT_STRENGTH0, &data, 1);
            }
        }
        else
        {

        }
    }
    return sts;
}

/**
 * @brief This API configures the polarity inversion of pins defined as 
 * inputs.
 */
int8_t pcal6408a_set_pol_inv(pcal6408a_dev *hdev, pcal6408a_gpio_pin pin, pcal6408a_pin_pol polarity)
{
    uint8_t data = 0;
    int8_t sts = PCAL6408A_E_NULL_PTR;
    sts = is_gpio_valid(pin);
    if (PCAL6408A_OK != sts)
    {
        sts = PCAL6408A_E_OUT_OF_RANGE;
    }
    else
    {
        sts = pcal6408a_read(hdev, PCAL6408_REG_POLARITY_INVERSION, &data, 1);
    }
    if (PCAL6408A_OK == sts)
    {
        data = bit_write(data, pin, polarity);
        sts = pcal6408a_write(hdev, PCAL6408_REG_POLARITY_INVERSION, &data, 1);
    }
    else
    {

    }
    return sts;
}

/**
 * @brief This API enables or disables the input latch of the corresponding 
 * gpio pin. This is effective only when the pin is configured as an INPUT
 */
int8_t pcal6408a_set_input_latch(pcal6408a_dev *hdev, pcal6408a_gpio_pin pin, pcal6408a_pin_lat latch)
{
    uint8_t data = 0;
    int8_t sts = PCAL6408A_E_NULL_PTR;
    sts = is_gpio_valid(pin);
    if (PCAL6408A_OK != sts)
    {
        sts = PCAL6408A_E_OUT_OF_RANGE;
    }
    else
    {
        sts = pcal6408a_read(hdev, PCAL6408_REG_INPUT_LATCH, &data, 1);
    }

    if (PCAL6408A_OK == sts)
    {
        data = bit_write(data, pin, latch);
        sts = pcal6408a_write(hdev, PCAL6408_REG_INPUT_LATCH, &data, 1);
    }
    else
    {

    }

    return sts;
}

/**
 * @brief This API enables or disables the input latch of the corresponding
 * gpio pin. This is effective only when the pin is configured as an INPUT
 */
pcal6408a_pin_lat pcal6408a_get_input_latch(pcal6408a_dev *hdev, pcal6408a_gpio_pin pin)
{
    uint8_t data = 0;
    uint8_t sts= PCAL6408A_E_NULL_PTR;
    sts = is_gpio_valid(pin);
    if (PCAL6408A_OK != sts)
    {
        sts = PCAL6408A_E_OUT_OF_RANGE;
    }
    else
    {
        sts = pcal6408a_read(hdev,PCAL6408_REG_INPUT_LATCH,&data,1);
        data = pin & data;
        if(data>0)
        {
            sts = LATCH;
        }
        else
        {
            sts = NO_LATCH;
        }
    }
    return sts;
}

/**
 * @brief This API enables or disables pull-up/pull-down resistors on the
 * corresponding gpio.
 */
int8_t pcal6408a_enable_pull(pcal6408a_dev *hdev, pcal6408a_gpio_pin pin, pcal6408a_pin_pull flag)
{
    uint8_t data = 0;
    int8_t sts = PCAL6408A_E_NULL_PTR;
    sts = is_gpio_valid(pin);
    if (PCAL6408A_OK != sts)
    {
        sts = PCAL6408A_E_OUT_OF_RANGE;
    }
    else
    {
        sts = pcal6408a_read(hdev, PCAL6408_REG_PULL_ENABLE, &data, 1);
        if (PCAL6408A_OK == sts)
        {
            data = bit_write(data, pin, flag);
            sts = pcal6408a_write(hdev, PCAL6408_REG_PULL_ENABLE, &data, 1);
        }
        else
        {

        }
    }

    return sts;
}

/**
 * @brief This API configures the gpio to have pull-up or pull-down resistor by
 * programming the pull-up/pull-down selection register.
 */
int8_t pcal6408a_set_pull_up_dwn(pcal6408a_dev *hdev, pcal6408a_gpio_pin pin, pcal6408a_pin_up_dwn flag)
{
    uint8_t data = 0;
    int8_t sts = PCAL6408A_E_NULL_PTR;
    sts = is_gpio_valid(pin);
    if (PCAL6408A_OK != sts)
    {
        sts = PCAL6408A_E_OUT_OF_RANGE;
    }
    else
    {
        if(flag==NO_PULL)
        {
            pcal6408a_enable_pull(hdev,pin,PULL_DISABLED);
        }
        else
        {
            pcal6408a_enable_pull(hdev,pin,PULL_ENABLED);
            sts = pcal6408a_read(hdev, PCAL6408_REG_PULL_UP_DOWN, &data, 1);
            if (PCAL6408A_OK == sts)
            {
                data = bit_write(data, pin, flag);
                sts = pcal6408a_write(hdev, PCAL6408_REG_PULL_UP_DOWN, &data, 1);
            }
            else
            {

            }
        }
    }

    return sts;
}

/**
 * @brief This API configures the interrupt mask of the corresponding gpio pin
 */
int8_t pcal6408a_intr_config(pcal6408a_dev *hdev, pcal6408a_gpio_pin pin, pcal6408a_intr_mode intr_mode)
{
    uint8_t data = 0;
    int8_t sts = PCAL6408A_E_NULL_PTR;
    sts = is_gpio_valid(pin);
    if (PCAL6408A_OK != sts)
    {
        sts = PCAL6408A_E_OUT_OF_RANGE;
    }
    else
    {
        if(intr_mode<INTRUPT_DISABLE)
        {
            sts = pcal6408a_read(hdev, PCAL6408_REG_INT_MASK, &data, 1);
            if (PCAL6408A_OK == sts)
            {
                data = bit_write(data,pin,0);
                sts = pcal6408a_write(hdev, PCAL6408_REG_INT_MASK, &data, 1);
                pcal6408a_set_input_latch(hdev, pin, LATCH);
            }
        }
    }
    return sts;
}

/**
 * @brief This API is used to identify the source of an interrupt.
 */
int8_t pcal6408a_read_intr_sts(pcal6408a_dev *hdev,uint8_t* intr_flg)
{
    int8_t sts=PCAL6408A_E_COM_FAIL;
    sts = pcal6408a_read(hdev, PCAL6408_REG_INT_STATUS, intr_flg, 1);
    return sts;
}

/**
 * @brief This API configures the output port as push-pull or open-drain.
 */
int8_t pcal6408a_set_port_cfg(pcal6408a_dev *hdev, pcal6408a_gpio_pin pin, pcal6408a_port_cfg flag)
{
    uint8_t data = 0;
    int8_t sts = PCAL6408A_E_NULL_PTR;
    sts = is_gpio_valid(pin);
    if (PCAL6408A_OK != sts)
    {
        sts = PCAL6408A_E_OUT_OF_RANGE;
    }
    else
    {
        sts = pcal6408a_read(hdev, PCAL6408_REG_OUT_CONFIG, &data, 1);
        if (PCAL6408A_OK == sts)
        {
            data = bit_write(data, pin, flag);
            sts = pcal6408a_write(hdev, PCAL6408_REG_OUT_CONFIG, &data, 1);
        }
        else
        {

        }
    }
    return sts;
}

/**
 * @brief This API is used to get the configurations parameters of the gpio pin.
 */
int8_t pcal6408a_gpio_pin_cfg(pcal6408a_dev *hdev, pcal6408a_gpio_cfg *gpio_config)
{
    int8_t sts = PCAL6408A_E_NULL_PTR;
    uint8_t gpio_bit_msk, pin=0;
    if(NULL == gpio_config || NULL == hdev)
    {
        sts = PCAL6408A_E_NULL_PTR;
    }
    else
    {
        switch(gpio_config->mode)
        {
            case PCAL6408A_GPIO_MODE_INPUT:
                {
                    sts = pcal6408a_set_dir(hdev,gpio_config->pin_num,INPUT);
                    break;
                }
            case PCAL6408A_GPIO_MODE_OUTPUT:
                {
                    sts = pcal6408a_set_dir(hdev,gpio_config->pin_num,OUTPUT);
                    break;
                }
            case PCAL6408A_GPIO_MODE_IT_RISING:
                {
                    sts = pcal6408a_set_dir(hdev,gpio_config->pin_num,INPUT);
                    sts |= pcal6408a_intr_config(hdev,gpio_config->pin_num,INTRUPT_RISING);
                    break;
                }
            case PCAL6408A_GPIO_MODE_IT_FALLING:
                {
                    sts = pcal6408a_set_dir(hdev,gpio_config->pin_num,INPUT);
                    sts |= pcal6408a_intr_config(hdev,gpio_config->pin_num,INTRUPT_FALLING);
                    break;
                }
            case PCAL6408A_GPIO_MODE_IT_RISING_FALLING:
                {
                    sts = pcal6408a_set_dir(hdev,gpio_config->pin_num,INPUT);
                    sts |= pcal6408a_intr_config(hdev,gpio_config->pin_num,INTRUPT_ANY_EDGE);
                    break;
                }
        }
        if(sts == PCAL6408A_SCS)
        {
            sts = pcal6408a_set_pull_up_dwn(hdev,gpio_config->pin_num,gpio_config->pull_sts);

            gpio_bit_msk=gpio_config->pin_num;
            while(gpio_bit_msk)
            {
                if((gpio_bit_msk & 0x01) == 0x01)
                {
                    hdev->intr_info[pin].callback_func=gpio_config->cb_func;
                    hdev->intr_info[pin].cb_func_args=gpio_config->cb_func_args;
                    hdev->intr_info[pin].intr_typ =gpio_config->mode;
                }
                gpio_bit_msk=gpio_bit_msk>>1;
                pin++;
            }
        }
        else
        {
            sts = PCAL6408A_E_NULL_PTR;
        }
    }
    return sts;
}

/**
 * @brief This utility API is used to set or reset a bit in the data
 */
static uint8_t bit_write (uint8_t data, pcal6408a_gpio_pin pin, uint8_t flag)
{
    if(flag)
    {
        data = (data | (pin));
    }
    else
    {
        data = (data & ~(pin));
    }
    return data;
}

/**
 * @brief This utility API is used to check the validity of the gpio pin number
 */
static int8_t is_gpio_valid (pcal6408a_gpio_pin pin)
{
    int8_t sts = PCAL6408A_E_NULL_PTR;

    if((PCAL6408A_GPIO_MAX < pin))
    {
        sts = PCAL6408A_E_OUT_OF_RANGE;
    }
    else
    {
        sts = PCAL6408A_OK;
    }

    return sts;
}
