#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_i2c.h"

#include "hal.h"
#include "i2c.h"
#include "delay.h"

struct _i2c_port {
    I2C_TypeDef *i2c;
};

i2c_port DEFAULT_I2C_PORT = {
        .i2c = I2C_PORT,
};

void i2c_init()
{
    GPIO_InitTypeDef gpio_init;

    gpio_init.GPIO_Pin = I2C_PIN_SCL;
    gpio_init.GPIO_Mode = GPIO_Mode_AF_OD;
    gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(I2C_GPIO, &gpio_init);

    gpio_init.GPIO_Pin = I2C_PIN_SDA;
    gpio_init.GPIO_Mode = GPIO_Mode_AF_OD;
    gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(I2C_GPIO, &gpio_init);

    RCC_APB1PeriphClockCmd(I2C_PORT_RCC_PERIPH, ENABLE);

    I2C_SoftwareResetCmd(I2C_PORT, ENABLE);
    delay_ms(2);
    I2C_SoftwareResetCmd(I2C_PORT, DISABLE);

    RCC_APB1PeriphResetCmd(I2C_PORT_RCC_PERIPH, ENABLE);
    delay_ms(2);
    RCC_APB1PeriphResetCmd(I2C_PORT_RCC_PERIPH, DISABLE);

    // NOTE: I2C chip reset is necessary here!
    I2C_DeInit(I2C_PORT);

    delay_ms(2);

    I2C_InitTypeDef i2c_init;
    I2C_StructInit(&i2c_init);

    i2c_init.I2C_ClockSpeed = 10000;
    i2c_init.I2C_Mode = I2C_Mode_I2C;
    i2c_init.I2C_DutyCycle = I2C_DutyCycle_2;
    i2c_init.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    i2c_init.I2C_Ack = I2C_Ack_Enable;

    I2C_Init(I2C_PORT, &i2c_init);

    // This loop may get stuck if there is nothing connected in the I²C port pins
    int count = 1000;
    while (I2C_GetFlagStatus(I2C_PORT, I2C_FLAG_BUSY) && count--) {
        delay_ms(1);
    }

    I2C_Cmd(I2C_PORT, ENABLE);
}

void i2c_uninit()
{
    I2C_Cmd(I2C_PORT, DISABLE);
    I2C_DeInit(I2C_PORT);
    RCC_APB1PeriphClockCmd(I2C_PORT_RCC_PERIPH, DISABLE);
}

static int i2c_prepare_op(i2c_port *port, uint8_t address)
{
    I2C_AcknowledgeConfig(port->i2c, ENABLE);
    I2C_GenerateSTART(port->i2c, ENABLE);

    while (!I2C_CheckEvent(port->i2c, I2C_EVENT_MASTER_MODE_SELECT));
    I2C_Send7bitAddress(port->i2c, address, I2C_Direction_Transmitter);

    while (!I2C_CheckEvent(port->i2c, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    return HAL_OK;
}

static int i2c_prepare_op_with_register(i2c_port *port, uint8_t address, uint8_t reg)
{
    i2c_prepare_op(port, address);

    I2C_SendData(port->i2c, reg);
    while (!I2C_CheckEvent(port->i2c, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    return HAL_OK;
}

static int i2c_prepare_read(i2c_port *port, uint8_t address, uint8_t reg)
{
    i2c_prepare_op_with_register(port, address, reg);

    I2C_GenerateSTART(port->i2c, ENABLE);
    while (!I2C_CheckEvent(port->i2c, I2C_EVENT_MASTER_MODE_SELECT));

    I2C_Send7bitAddress(port->i2c, address, I2C_Direction_Receiver);
    while (!I2C_CheckEvent(port->i2c, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

    return HAL_OK;
}

static int i2c_finish_read(i2c_port *port)
{
    I2C_GenerateSTOP(port->i2c, ENABLE);
    I2C_AcknowledgeConfig(port->i2c, DISABLE);

    while (!I2C_CheckEvent(port->i2c, I2C_EVENT_MASTER_BYTE_RECEIVED));
    I2C_ReceiveData(port->i2c);

    return HAL_OK;
}

static int i2c_prepare_write(i2c_port *port, uint8_t address, uint8_t reg)
{
    return i2c_prepare_op_with_register(port, address, reg);
}

static int i2c_finish_write(i2c_port *port)
{
    I2C_GenerateSTOP(port->i2c, ENABLE);

    while (!I2C_CheckEvent(port->i2c, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    return HAL_OK;
}

int i2c_read_bytes(i2c_port *port, uint8_t address, uint8_t reg, uint8_t size, uint8_t *data)
{
    i2c_prepare_read(port, address, reg);

    for (uint8_t i = 0; i < size; i++) {
        while (!I2C_CheckEvent(port->i2c, I2C_EVENT_MASTER_BYTE_RECEIVED));
        data[i] = I2C_ReceiveData(port->i2c);
    }

    i2c_finish_read(port);

    return HAL_OK;
}

int i2c_read_byte(i2c_port *port, uint8_t address, uint8_t reg, uint8_t *data)
{
    return i2c_read_bytes(port, address, reg, 1, data);
}

int i2c_write_bytes(i2c_port *port, uint8_t address, uint8_t reg, uint8_t size, uint8_t *data)
{
    i2c_prepare_write(port, address, reg);

    for (uint8_t i = 0; i < size; i++) {
        I2C_SendData(port->i2c, data[i]);
        while (!I2C_CheckEvent(port->i2c, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    }

    i2c_finish_write(port);

    return HAL_OK;
}

int i2c_write_byte(i2c_port *port, uint8_t address, uint8_t reg, uint8_t data)
{
    return i2c_write_bytes(port, address, reg, 1, &data);
}
