/*
 * irda.c
 *
 * had a nice protocol planned, but no time. so we bruteforce it
 *
 * transmitter will spew packets with the following data:
 * 2-byte header 0x5aa5
 * 2-byte length, though messages cannot be longer than 248 bytes currently
 * 2-byte checksum of message content
 * message content
 *
 * if a receiver receives the data, and checksum passes, it will flash or something.
 * receiver will respond with the following data:
 * 2-byte header 0xa55a (swapped bytes from transmit header)
 * 2-byte ascii 'OK'
 * 2-byte 0xffff xor checksum of the sent content
 *
 * if the transmitter sees the OK packet with inverted checksum, it will flash or something.
 * if transmitter sees anything else, it will be ignored and cleared.
 *
 * transmission is handled via DMA.
 * reception is handled via interrupt.
 *
 */

#include "irda.h"

#include "intr_out.h"
#include "../i2c_slave.h"



static DMA_InitTypeDef tx_dma = {0};

static uint16_t irda_timeout;
static uint8_t  irda_state;

volatile uint8_t *irda_tx_buf;
uint16_t irda_tx_len;
uint16_t irda_tx_timer;
uint8_t  irda_tx_hdr[6];    // contains header, length, and checksum

volatile uint8_t *irda_rx_buf;
uint16_t irda_rx_len;       // filled after receiving header
uint16_t irda_rx_csum;      // filled after receiving header
uint16_t irda_rx_pos;
uint16_t irda_rx_timeout;
uint8_t  irda_rx_hdr[6];



void irda_init()
{
    USART_InitTypeDef usart = {0};

    IrDA_Shutdown();

    // buffers
    irda_tx_buf = &i2cs_reg[0x100];
    irda_rx_buf = &i2cs_reg[0x200];

    // configure USART peripheral
    usart.USART_BaudRate   = 115200;
    usart.USART_WordLength = USART_WordLength_8b;
    usart.USART_StopBits   = USART_StopBits_1;
    usart.USART_Parity     = USART_Parity_No;
    usart.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    usart.USART_Mode       = 0; // USART_Mode_Tx | USART_Mode_Rx;
    USART_Init(USART1, &usart);

    USART_IrDACmd(USART1, ENABLE);

    // configure and enable USART interrupt
    NVIC_SetPriority(USART1_IRQn, 2 << 6);
    NVIC_EnableIRQ(USART1_IRQn);

    // configure transmitter DMA
    DMA_DeInit(DMA1_Channel4);
    tx_dma.DMA_PeripheralBaseAddr = (u32)(&USART1->DATAR);
    tx_dma.DMA_DIR                = DMA_DIR_PeripheralDST;
    tx_dma.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
    tx_dma.DMA_MemoryInc          = DMA_MemoryInc_Enable;
    tx_dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    tx_dma.DMA_MemoryDataSize     = DMA_MemoryDataSize_Byte;
    tx_dma.DMA_Mode               = DMA_Mode_Normal;
    tx_dma.DMA_Priority           = DMA_Priority_High;
    tx_dma.DMA_M2M                = DMA_M2M_Disable;


    USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);

    // configure and enable transmitter DMA complete interrupt
    DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE);
}

void irda_ena_tx()
{
    USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);

    USART1->CTLR2 &= ~USART_CTLR1_RE;
    USART1->CTLR2 |=  USART_CTLR1_TE;
}

void irda_ena_rx()
{
    USART1->CTLR2 &= ~USART_CTLR1_TE;
    USART1->CTLR2 |=  USART_CTLR1_RE;

    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
}

uint16_t irda_csum(uint8_t *dat, uint16_t len)
{
    uint16_t i;
    uint16_t csum = 0;

    for (i = 0; i < len; i++) {
        csum += *dat++;
    }

    return csum;
}

// starts or restarts a transmission
void irda_restart_tx(uint8_t ack)
{
    uint16_t csum;

    if ((!i2cs_reg[REG_IRDA_WRITE_HI] && (i2cs_reg[REG_IRDA_WRITE_LO] <= 248)) || ack) {
        // stop any transmit
        USART_DMACmd(USART1, USART_DMAReq_Tx, DISABLE);

        if (!ack) {
            // standard data header
            irda_tx_hdr[0] = IRDA_HEADER_01;
            irda_tx_hdr[1] = IRDA_HEADER_02;
            irda_tx_hdr[2] = 0;
            irda_tx_hdr[3] = i2cs_reg[REG_IRDA_WRITE_LO];

            csum = irda_csum((uint8_t *)irda_tx_buf, irda_tx_hdr[3]);
            irda_tx_hdr[4] = csum >> 8;
            irda_tx_hdr[5] = csum & 0xff;

            // we'll try again after some time
            irda_tx_timer = i2cs_reg[REG_IRDA_WRITE_RETRY] * 2;

            // set data length
            irda_tx_len = i2cs_reg[REG_IRDA_WRITE_LO];
        } else {
            // rx ack header
            irda_tx_hdr[0] = IRDA_HEADER_02;
            irda_tx_hdr[1] = IRDA_HEADER_01;
            irda_tx_hdr[2] = 'O';
            irda_tx_hdr[3] = 'K';

            irda_tx_hdr[4] = irda_rx_hdr[4] ^ 0xff;
            irda_tx_hdr[5] = irda_rx_hdr[5] ^ 0xff;

            irda_tx_timer = 200 / 6;    // acks are always sent 9 times
            irda_tx_len = 0;
        }

        // prepare the new write
        tx_dma.DMA_MemoryBaseAddr = (uint32_t)irda_tx_hdr;
        tx_dma.DMA_BufferSize = 6;
        DMA_Init(DMA1_Channel4, &tx_dma);

        // then go for it
        USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
    } else {
        // data to write is too long; set transmit error interrupt flag
        i2cs_append_flag(INT_UTX_ERROR);
    }
}

void irda_stop()
{
    // disable all related internal peripherals
    USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
    USART_DMACmd(USART1, USART_DMAReq_Tx, DISABLE);

    USART_Cmd(USART1, DISABLE);

    // reset internal state
    irda_state = IRDA_STATE_IDLE;
    irda_timeout = 0;

    // disable the PHY
    IrDA_Shutdown();

    // reset register mode
    i2cs_reg[REG_IRDA_MODE] = 0;
}


void irda_mode(uint8_t mode, uint16_t timeout)
{
    irda_state = mode;

    switch (mode) {
        case IRDA_STATE_IDLE: {
            // cancel any pending operations
            irda_stop();
            break;
        }

        case IRDA_STATE_TX_HEADER: {
            IrDA_Enable();
            irda_ena_tx();

            irda_timeout = timeout;
            irda_restart_tx(0);
            break;
        }
        case IRDA_STATE_TX_ACK: {
            IrDA_Enable();
            irda_ena_tx();

            irda_timeout = 300;         // we only send acks for a little over a second
            irda_restart_tx(1);
            break;
        }

        case IRDA_STATE_RX_DATA:
        case IRDA_STATE_RX_ACK: {
            irda_rx_pos = 0;
            irda_rx_len = 0;

            IrDA_Enable();
            irda_ena_rx();

            irda_timeout = timeout;
        }
    }
}

void irda_process()
{
    if (!irda_timeout) {
        switch (irda_state) {
            case IRDA_STATE_TX_HEADER:
            case IRDA_STATE_TX_BODY:
            case IRDA_STATE_TX_ACK: {
                // didn't complete in time; set timeout flag
                i2cs_append_flag(INT_UTX_TIMEOUT);
                break;
            }
            default: {
                // receiving just doesn't send an interrupt
                break;
            }
        }
        irda_stop();
        return;
    }

    irda_timeout--;

    switch (irda_state) {
        case IRDA_STATE_TX_HEADER:
        case IRDA_STATE_TX_BODY:
        case IRDA_STATE_TX_ACK: {
            if (!irda_tx_timer) {
                irda_restart_tx((irda_state == IRDA_STATE_TX_ACK) ? 1 : 0);
            } else {
                irda_tx_timer--;
            }
            break;
        }

        case IRDA_STATE_RX_DATA:
        case IRDA_STATE_RX_ACK: {
            if (irda_rx_pos) {
                if (irda_rx_timeout) {
                    irda_rx_timeout--;
                } else {
                    // we received some data but it didn't get completed
                    // so abort
                    irda_rx_pos = 0;
                }
            }
        }
    }
}

void USART1_IRQHandler(void)
{
    uint16_t csum;

    // receive
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        if (irda_rx_pos <= 248+6) {
            if (irda_rx_pos < 6) {
                irda_rx_hdr[irda_rx_pos] = USART_ReceiveData(USART1);
            } else {
                irda_rx_buf[irda_rx_pos - 6] = USART_ReceiveData(USART1);

                if ((irda_rx_pos - 6) == irda_rx_len) {
                    // full packet received. validate the checksum
                    csum = irda_csum((uint8_t *)irda_rx_buf, irda_rx_len);
                    if (((csum >> 8) == irda_rx_hdr[2]) && ((csum & 0xff) == irda_rx_hdr[3])) {
                        // checksum matches! let the host to commit the data
                        i2cs_reg[REG_IRDA_READ_HI] = 0;
                        i2cs_reg[REG_IRDA_READ_LO] = irda_rx_len;
                        i2cs_append_flag(INT_URX_RCVD);

                        // and let the transmitter know it was received successfully
                        irda_mode(IRDA_STATE_TX_ACK, 0);

                    }
                }
            }
            irda_rx_pos++;

            // validate the header
            if (irda_rx_pos == 6) {
                switch (irda_state) {
                    case IRDA_STATE_RX_DATA: {
                        // is the header valid?
                        if ((irda_rx_hdr[0] == IRDA_HEADER_01) && (irda_rx_hdr[1] == IRDA_HEADER_02)) {
                            // is the length valid?
                            if (!irda_rx_hdr[2] && (irda_rx_hdr[3] <= 248)) {
                                // good to go
                                irda_rx_len = irda_rx_hdr[3];
                                irda_rx_csum = (irda_rx_hdr[4] << 8) | irda_rx_hdr[5];
                                break;
                            }
                        }

                        // something isn't valid; cancel this rx
                        irda_rx_pos = 0;
                        irda_rx_len = 0;
                        break;
                    }
                    case IRDA_STATE_RX_ACK: {
                        irda_rx_len = 0;

                        // is the header valid?
                        if ((irda_rx_hdr[0] == IRDA_HEADER_02) && (irda_rx_hdr[1] == IRDA_HEADER_01)) {
                            // are the third and fourth bytes 'OK'?
                            if ((irda_rx_hdr[2] == 'O') && (irda_rx_hdr[3] == 'K')) {
                                // how about the checksum?
                                csum = (irda_tx_hdr[4] << 8) | irda_tx_hdr[5];
                                csum ^= 0xffff;
                                if (csum == irda_rx_csum) {
                                    // seems good; let the master know
                                    irda_stop();
                                    i2cs_append_flag(INT_UTX_DONE);
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

void DMA1_Channel4_IRQHandler(void)
{
    if (DMA_GetITStatus(DMA1_IT_TC4)) {
        if (tx_dma.DMA_MemoryBaseAddr == (uint32_t)irda_tx_hdr) {
            if (irda_tx_len) {
                tx_dma.DMA_MemoryBaseAddr = (uint32_t)irda_tx_buf;
                tx_dma.DMA_BufferSize = irda_tx_len;

                USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
            }
        }
    }

    DMA_ClearITPendingBit(DMA1_IT_GL4 | DMA1_IT_TC4 | DMA1_IT_HT4 | DMA1_IT_TE4);
}
