/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 <xc.h>
#include <os/mynewt.h>
#include <hal/hal_gpio.h>
#include <mcu/mips_hal.h>

#define GPIO_INDEX(pin)     ((pin) & 0x0F)
#define GPIO_PORT(pin)      (((pin) >> 4) & 0x0F)
#define GPIO_MASK(pin)      (1 << GPIO_INDEX(pin))

#define LATxCLR(P)      (base_address[P].gpio[0x14 / 0x4])
#define LATxSET(P)      (base_address[P].gpio[0x18 / 0x4])
#define LATxINV(P)      (base_address[P].gpio[0x1C / 0x4])
#define PORTx(P)        (base_address[P].gpio[0x0])
#define CNPUxCLR(P)     (base_address[P].gpio[0x34 / 0x4])
#define CNPUxSET(P)     (base_address[P].gpio[0x38 / 0x4])
#define CNPDxCLR(P)     (base_address[P].gpio[0x44 / 0x4])
#define CNPDxSET(P)     (base_address[P].gpio[0x48 / 0x4])
#define ODCxCLR(P)      (base_address[P].gpio[0x24 / 0x4])
#define CNCONxSET(P)    (base_address[P].gpio[0x58 / 0x4])
#define CNENxCLR(P)     (base_address[P].gpio[0x64 / 0x4])
#define CNENxSET(P)     (base_address[P].gpio[0x68 / 0x4])
#define CNNExCLR(P)     (base_address[P].gpio[0x84 / 0x4])
#define CNNExSET(P)     (base_address[P].gpio[0x88 / 0x4])
#define CNFx(P)         (base_address[P].gpio[0x90 / 0x4])
#define CNFxCLR(P)      (base_address[P].gpio[0x94 / 0x4])
#define ANSELxCLR(P)    (base_address[P].ansel[0x04 / 0x4])
#define TRISxCLR(P)     (base_address[P].tris[0x04 / 0x4])
#define TRISxSET(P)     (base_address[P].tris[0x08 / 0x4])

struct hal_gpio_irq_t {
    int                    pin;
    hal_gpio_irq_trig_t    trig;
    hal_gpio_irq_handler_t handler;
    void                   *arg;
};

#define HAL_GPIO_MAX_IRQ    (8)
static struct hal_gpio_irq_t hal_gpio_irqs[HAL_GPIO_MAX_IRQ];

struct pic32_gpio_t {
    volatile uint32_t * gpio;
    volatile uint32_t * ansel;
    volatile uint32_t * tris;
};

static uint32_t dummy_reg[2];
#ifndef ANSELA
#define ANSELA dummy_reg[0]
#endif
#ifndef ANSELC
#define ANSELC dummy_reg[0]
#endif
#ifndef ANSELD
#define ANSELD dummy_reg[0]
#endif
#ifndef ANSELE
#define ANSELE dummy_reg[0]
#endif
#ifndef ANSELF
#define ANSELF dummy_reg[0]
#endif
#ifndef ANSELH
#define ANSELH dummy_reg[0]
#endif
#ifndef ANSELJ
#define ANSELJ dummy_reg[0]
#endif
#ifndef ANSELK
#define ANSELK dummy_reg[0]
#endif
#ifndef _PORTA_BASE_ADDRESS
#define _PORTA_BASE_ADDRESS dummy_reg
#endif
#ifndef _PORTH_BASE_ADDRESS
#define _PORTH_BASE_ADDRESS dummy_reg
#endif
#ifndef _PORTJ_BASE_ADDRESS
#define _PORTJ_BASE_ADDRESS dummy_reg
#endif
#ifndef _PORTK_BASE_ADDRESS
#define _PORTK_BASE_ADDRESS dummy_reg
#endif
#ifndef TRISA
#define TRISA dummy_reg[0]
#endif
#ifndef TRISH
#define TRISH dummy_reg[0]
#endif
#ifndef TRISJ
#define TRISJ dummy_reg[0]
#endif
#ifndef TRISK
#define TRISK dummy_reg[0]
#endif

static const struct pic32_gpio_t base_address[] = {
    {
        .gpio  = (volatile uint32_t *)_PORTA_BASE_ADDRESS,
        .ansel = (volatile uint32_t *)&ANSELA,
        .tris  = (volatile uint32_t *)&TRISA
    },
    {
        .gpio  = (volatile uint32_t *)_PORTB_BASE_ADDRESS,
        .ansel = (volatile uint32_t *)&ANSELB,
        .tris  = (volatile uint32_t *)&TRISB
    },
    {
        .gpio  = (volatile uint32_t *)_PORTC_BASE_ADDRESS,
        .ansel = (volatile uint32_t *)&ANSELC,
        .tris  = (volatile uint32_t *)&TRISC
    },
    {
        .gpio  = (volatile uint32_t *)_PORTD_BASE_ADDRESS,
        .ansel = (volatile uint32_t *)&ANSELD,
        .tris  = (volatile uint32_t *)&TRISD
    },
    {
        .gpio  = (volatile uint32_t *)_PORTE_BASE_ADDRESS,
        .ansel = (volatile uint32_t *)&ANSELE,
        .tris  = (volatile uint32_t *)&TRISE
    },
    {
        .gpio  = (volatile uint32_t *)_PORTF_BASE_ADDRESS,
        .ansel = (volatile uint32_t *)&ANSELF,
        .tris  = (volatile uint32_t *)&TRISF
    },
    {
        .gpio  = (volatile uint32_t *)_PORTG_BASE_ADDRESS,
        .ansel = (volatile uint32_t *)&ANSELG,
        .tris  = (volatile uint32_t *)&TRISG
    },
    {
        .gpio  = (volatile uint32_t *)_PORTH_BASE_ADDRESS,
        .ansel = (volatile uint32_t *)&ANSELH,
        .tris  = (volatile uint32_t *)&TRISH
    },
    {
        .gpio  = (volatile uint32_t *)_PORTJ_BASE_ADDRESS,
        .ansel = (volatile uint32_t *)&ANSELJ,
        .tris  = (volatile uint32_t *)&TRISJ
    },
    {
        .gpio  = (volatile uint32_t *)_PORTK_BASE_ADDRESS,
        .ansel = (volatile uint32_t *)&ANSELK,
        .tris  = (volatile uint32_t *)&TRISK
    },
};

static uint8_t
hal_gpio_find_pin(int pin)
{
    uint8_t index = 0;

    while (index < HAL_GPIO_MAX_IRQ) {
        if (hal_gpio_irqs[index].pin == pin) {
            break;
        }

        ++index;
    }

    return index;
}

static uint8_t
hal_gpio_find_empty_slot(void)
{
    uint8_t index = 0;

    while (index < HAL_GPIO_MAX_IRQ) {
        if (hal_gpio_irqs[index].handler == NULL) {
            break;
        }

        ++index;
    }

    return index;
}

static void
hal_gpio_handle_isr(uint32_t port)
{
    uint32_t mask, val;
    uint8_t index;

    for (index = 0; index < HAL_GPIO_MAX_IRQ; ++index) {

        if (hal_gpio_irqs[index].handler == NULL) {
            continue;
        }
        if (GPIO_PORT(hal_gpio_irqs[index].pin) != port) {
            continue;
        }

        mask = GPIO_MASK(hal_gpio_irqs[index].pin);
        if ((CNFx(port) & mask) != mask) {
            continue;
        }

        val = PORTx(port) & mask;
        if ((val && (hal_gpio_irqs[index].trig & HAL_GPIO_TRIG_RISING)) ||
            (!val && (hal_gpio_irqs[index].trig & HAL_GPIO_TRIG_FALLING))) {
            hal_gpio_irqs[index].handler(hal_gpio_irqs[index].arg);
        }
        CNFxCLR(port) = mask;
    }
}

#ifdef _PORTA
void
__attribute__((interrupt(IPL1AUTO), vector(_CHANGE_NOTICE_A_VECTOR)))
hal_gpio_porta_isr(void)
{
    hal_gpio_handle_isr(0);
    IFS3CLR = _IFS3_CNAIF_MASK;
}
#endif

void
__attribute__((interrupt(IPL1AUTO), vector(_CHANGE_NOTICE_B_VECTOR)))
hal_gpio_portb_isr(void)
{
    hal_gpio_handle_isr(1);
    IFS3CLR = _IFS3_CNBIF_MASK;
}

void
__attribute__((interrupt(IPL1AUTO), vector(_CHANGE_NOTICE_C_VECTOR)))
hal_gpio_portc_isr(void)
{
    hal_gpio_handle_isr(2);
    IFS3CLR = _IFS3_CNCIF_MASK;
}

void
__attribute__((interrupt(IPL1AUTO), vector(_CHANGE_NOTICE_D_VECTOR)))
hal_gpio_portd_isr(void)
{
    hal_gpio_handle_isr(3);
    IFS3CLR = _IFS3_CNDIF_MASK;
}

void
__attribute__((interrupt(IPL1AUTO), vector(_CHANGE_NOTICE_E_VECTOR)))
hal_gpio_porte_isr(void)
{
    hal_gpio_handle_isr(4);
    IFS3CLR = _IFS3_CNEIF_MASK;
}

void
__attribute__((interrupt(IPL1AUTO), vector(_CHANGE_NOTICE_F_VECTOR)))
hal_gpio_portf_isr(void)
{
    hal_gpio_handle_isr(5);
    IFS3CLR = _IFS3_CNFIF_MASK;
}

void
__attribute__((interrupt(IPL1AUTO), vector(_CHANGE_NOTICE_G_VECTOR)))
hal_gpio_portg_isr(void)
{
    hal_gpio_handle_isr(6);
    IFS3CLR = _IFS3_CNGIF_MASK;
}

int
hal_gpio_init_in(int pin, hal_gpio_pull_t pull)
{
    uint32_t port = GPIO_PORT(pin);
    uint32_t mask = GPIO_MASK(pin);

    /* Configure pin as digital */
    ANSELxCLR(port) = mask;

    ODCxCLR(port) = mask;

    switch (pull) {
    case HAL_GPIO_PULL_NONE:
        CNPUxCLR(port) = mask;
        CNPDxCLR(port) = mask;
        break;

    case HAL_GPIO_PULL_DOWN:
        CNPUxCLR(port) = mask;
        CNPDxSET(port) = mask;
        break;

    case HAL_GPIO_PULL_UP:
        CNPUxSET(port) = mask;
        CNPDxCLR(port) = mask;
        break;

    default:
        return -1;
    }

    /* Configure pin direction as input */
    TRISxSET(port) = mask;

    return 0;
}

int
hal_gpio_init_out(int pin, int val)
{
    uint32_t port = GPIO_PORT(pin);
    uint32_t mask = GPIO_MASK(pin);

    /* Configure pin as digital */
    ANSELxCLR(port) = mask;

    /* Disable pull-up, pull-down and open drain */
    CNPUxCLR(port) = mask;
    CNPDxCLR(port) = mask;
    ODCxCLR(port) = mask;

    if (val) {
        LATxSET(port) = mask;
    } else {
        LATxCLR(port) = mask;
    }

    /* Configure pin direction as output */
    TRISxCLR(port) = mask;

    return 0;
}

int
hal_gpio_deinit(int pin)
{
    uint32_t port = GPIO_PORT(pin);
    uint32_t mask = GPIO_MASK(pin);

    /* Disable pull-up, pull-down and open drain */
    CNPUxCLR(port) = mask;
    CNPDxCLR(port) = mask;
    ODCxCLR(port) = mask;

    /* Configure pin direction as input */
    TRISxSET(port) = mask;

    return 0;
}

void
hal_gpio_write(int pin, int val)
{
    uint32_t port = GPIO_PORT(pin);
    uint32_t mask = GPIO_MASK(pin);

    if (val) {
        LATxSET(port) = mask;
    } else {
        LATxCLR(port) = mask;
    }
}

int
hal_gpio_read(int pin)
{
    uint32_t port = GPIO_PORT(pin);
    uint32_t mask = GPIO_MASK(pin);

    return !!(PORTx(port) & mask);
}

int
hal_gpio_toggle(int pin)
{
    uint32_t port = GPIO_PORT(pin);
    uint32_t mask = GPIO_MASK(pin);

    LATxINV(port) = mask;

    /*
     * One instruction cycle is required between a write and a read
     * operation on the same port.
     */
    asm volatile ("nop");

    return !!(PORTx(port) & mask);
}

int
hal_gpio_irq_init(int pin, hal_gpio_irq_handler_t handler, void *arg,
                  hal_gpio_irq_trig_t trig, hal_gpio_pull_t pull)
{
    uint32_t ctx;
    int ret;
    uint8_t index;

    /* HAL_GPIO_TRIG_LOW and HAL_GPIO_TRIG_HIGH are not supported */
    if (trig == HAL_GPIO_TRIG_LOW ||
        trig == HAL_GPIO_TRIG_HIGH ||
        trig == HAL_GPIO_TRIG_NONE) {
        return -1;
    }

    /* Remove any existing irq handler attached to the pin */
    hal_gpio_irq_release(pin);
    hal_gpio_irq_disable(pin);

    index = hal_gpio_find_empty_slot();
    if (index == HAL_GPIO_MAX_IRQ) {
        return -1;
    }

    ret = hal_gpio_init_in(pin, pull);
    if (ret < 0) {
        return ret;
    }

    __HAL_DISABLE_INTERRUPTS(ctx);
    hal_gpio_irqs[index].arg = arg;
    hal_gpio_irqs[index].pin = pin;
    hal_gpio_irqs[index].trig = trig;
    hal_gpio_irqs[index].handler = handler;
    __HAL_ENABLE_INTERRUPTS(ctx);

    return 0;
}

void
hal_gpio_irq_release(int pin)
{
    uint32_t ctx;
    uint8_t index = hal_gpio_find_pin(pin);
    if (index == HAL_GPIO_MAX_IRQ) {
        return;
    }

    __HAL_DISABLE_INTERRUPTS(ctx);
    hal_gpio_irqs[index].handler = NULL;
    __HAL_ENABLE_INTERRUPTS(ctx);
}

void
hal_gpio_irq_enable(int pin)
{
    uint32_t port, mask, ctx;

    uint8_t index = hal_gpio_find_pin(pin);
    if (index == HAL_GPIO_MAX_IRQ)
        return;

    port = GPIO_PORT(pin);
    mask = GPIO_MASK(pin);

    __HAL_DISABLE_INTERRUPTS(ctx);

    /* Enable Change Notice module for the port */
    CNCONxSET(port) = _CNCONB_ON_MASK | _CNCONB_EDGEDETECT_MASK;

    switch (hal_gpio_irqs[index].trig) {
    case HAL_GPIO_TRIG_RISING:
        CNENxSET(port) = mask;
        break;
    case HAL_GPIO_TRIG_FALLING:
        CNNExSET(port) = mask;
        break;
    case HAL_GPIO_TRIG_BOTH:
        CNENxSET(port) = mask;
        CNNExSET(port) = mask;
        break;
    default:
        break;
    }

    /* Set interrupt priority */
    switch (port) {
#if defined(_PORTA)
    case 0:
        IPC29CLR = (_IPC29_CNAIP_MASK | _IPC29_CNAIS_MASK);
        IPC29 |= 1 << _IPC29_CNAIP_POSITION;
        break;
#endif
    case 1:
        IPC29CLR = (_IPC29_CNBIP_MASK | _IPC29_CNBIS_MASK);
        IPC29 |= 1 << _IPC29_CNBIP_POSITION;
        break;
    case 2:
        IPC30CLR = (_IPC30_CNCIP_MASK | _IPC30_CNCIS_MASK);
        IPC30 |= 1 << _IPC30_CNCIP_POSITION;
        break;
    case 3:
        IPC30CLR = (_IPC30_CNDIP_MASK | _IPC30_CNDIS_MASK);
        IPC30 |= 1 << _IPC30_CNDIP_POSITION;
        break;
    case 4:
        IPC30CLR = (_IPC30_CNEIP_MASK | _IPC30_CNEIS_MASK);
        IPC30 |= 1 << _IPC30_CNEIP_POSITION;
        break;
    case 5:
        IPC30CLR = (_IPC30_CNFIP_MASK | _IPC30_CNFIS_MASK);
        IPC30 |= 1 << _IPC30_CNFIP_POSITION;
        break;
    case 6:
        IPC31CLR = (_IPC31_CNGIP_MASK | _IPC31_CNGIS_MASK);
        IPC31 |= 1 << _IPC31_CNGIP_POSITION;
        break;
#ifdef _PORTH
    case 7:
        IPC31CLR = (_IPC31_CNHIP_MASK | _IPC31_CNHIS_MASK);
        IPC31 |= 1 << _IPC31_CNHIP_POSITION;
        break;
#endif
    }

    /* Clear interrupt flag and enable Change Notice interrupt */
    switch (port) {
#ifdef _PORTA
    case 0:
        IFS3CLR = _IFS3_CNAIF_MASK;
        IEC3SET = _IEC3_CNAIE_MASK;
        break;
#endif
    case 1:
        IFS3CLR = _IFS3_CNBIF_MASK;
        IEC3SET = _IEC3_CNBIE_MASK;
        break;
    case 2:
        IFS3CLR = _IFS3_CNCIF_MASK;
        IEC3SET = _IEC3_CNCIE_MASK;
        break;
    case 3:
        IFS3CLR = _IFS3_CNDIF_MASK;
        IEC3SET = _IEC3_CNDIE_MASK;
        break;
    case 4:
        IFS3CLR = _IFS3_CNEIF_MASK;
        IEC3SET = _IEC3_CNEIE_MASK;
        break;
    case 5:
        IFS3CLR = _IFS3_CNFIF_MASK;
        IEC3SET = _IEC3_CNFIE_MASK;
        break;
    case 6:
        IFS3CLR = _IFS3_CNGIF_MASK;
        IEC3SET = _IEC3_CNGIE_MASK;
        break;
#ifdef _PORTH
    case 7:
        IFS3CLR = _IFS3_CNHIF_MASK;
        IEC3SET = _IEC3_CNHIE_MASK;
        break;
#endif
    }

    __HAL_ENABLE_INTERRUPTS(ctx);
}

void
hal_gpio_irq_disable(int pin)
{
    uint32_t port = GPIO_PORT(pin);
    uint32_t mask = GPIO_MASK(pin);

    CNENxCLR(port) = mask;
    CNNExCLR(port) = mask;
}
