#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/neutrino.h>
#include <screen/screen.h>
#include <sys/keycodes.h>

#define RPI_GPIO_INTR   145

/**
 * Base physical address for the RaspberryPi 4 peripherals.
 * A different address can be specified with the -a command-line argument.
 */
static uintptr_t            base_paddr = 0xfe000000;

/**
 * Base virtual address of the mapped GPIO registers.
 */
uint32_t volatile          *gpio_regs;
/**
 * RaspberryPi GPIO registers.
 */
enum
{
    REG_GPSET0 = 7,
    REG_GPCLR0 = 10,
    REG_GPLEV0 = 13,
    REG_GPEDS0 = 16,
    REG_GPEDS1 = 17,
    REG_GPREN0 = 19,
    REG_GPREN1 = 20,
    REG_GPFEN0 = 22,
    REG_GPFEN1 = 23,
    REG_GPHEN0 = 25,
    REG_GPHEN1 = 26,
    REG_GPLEN0 = 28,
    REG_GPLEN1 = 29,
    REG_GPPUD = 37,
    REG_GPPUDCLK1 = 38,
    REG_GPPUDCLK2 = 39,
};

extern uint32_t volatile   *gpio_regs;

/**
 * Programs the FSEL register for the given GPIO.
 * The value is between 0 and 7 and determines the function of the GPIO. Typical
 * values are 0 for input and 1 for output.
 * @param   gpio    Pin number
 * @param   value   New pin mode
 */
static inline void
gpio_set_select(unsigned const gpio, unsigned const value)
{
    unsigned const  reg = gpio / 10;
    unsigned const  off = (gpio % 10) * 3;
    gpio_regs[reg] &= ~(7 << off);
    gpio_regs[reg] |= (value << off);
}

/**
 * Turns on a pin in output mode.
 * @param   gpio    Pin number
 */
static inline void
gpio_set(unsigned const gpio)
{
    unsigned const  reg = REG_GPSET0 + gpio / 32;
    unsigned const  off = gpio % 32;
    gpio_regs[reg] = (1 << off);
}

/**
 * Turns off a pin in output mode.
 * @param   gpio    Pin number
 */
static inline void
gpio_clear(unsigned gpio)
{
    unsigned const  reg = REG_GPCLR0 + gpio / 32;
    unsigned const  off = gpio % 32;
    gpio_regs[reg] = (1 << off);
}

/**
 * Sets or clears a pin.
 * @param   gpio    Pin number
 * @param   value   0 to clear, any other value to set
 */
static inline void
gpio_write(unsigned const gpio, unsigned const value)
{
    if (value) {
        gpio_set(gpio);
    } else {
        gpio_clear(gpio);
    }
}

/**
 * Reads the level of a pin in input mode.
 * @param   gpio    Pin number
 * @return  0 if the pin is low, 1 if high
 */
static inline unsigned
gpio_read(unsigned const gpio)
{
    unsigned const  reg = REG_GPLEV0 + gpio / 32;
    unsigned const  off = gpio % 32;
    return (gpio_regs[reg] & (1 << off)) >> off;
}

/**
 * Detect rising edge events on the given pin.
 * @param   gpio    Pin number
 */
static inline void
gpio_detect_rising_edge(unsigned const gpio)
{
    unsigned const  reg = REG_GPREN0 + gpio / 32;
    unsigned const  off = gpio % 32;
    gpio_regs[reg] = (1 << off);
}

/**
 * Detect falling edge events on the given pin.
 * @param   gpio    Pin number
 */
static inline void
gpio_detect_falling_edge(unsigned const gpio)
{
    unsigned const  reg = REG_GPFEN0 + gpio / 32;
    unsigned const  off = gpio % 32;
    gpio_regs[reg] = (1 << off);
}

/**
 * Detect rising edge events on the given pin.
 * @param   gpio    Pin number
 */
static inline void
gpio_clear_event(unsigned const gpio)
{
    unsigned const  reg = REG_GPEDS0 + gpio / 32;
    unsigned const  off = gpio % 32;
    gpio_regs[reg] = (1 << off);
}

int
main(int argc, char **argv)
{
    // Create an input provider context.
    screen_context_t context;
    if (screen_create_context(&context, SCREEN_INPUT_PROVIDER_CONTEXT) == -1) {
        perror("screen_create_context");
        return -1;
    }

    // Find a display.
    int ndisplays;
    if (screen_get_context_property_iv(context, SCREEN_PROPERTY_DISPLAY_COUNT,
                                       &ndisplays) == -1) {
        perror("screen_get_context_property_iv(display_count)");
        return 1;
    }

    if (ndisplays == 0) {
        fprintf(stderr, "No displays\n");
        return 1;
    }

    screen_display_t *displays = calloc(sizeof(screen_display_t), ndisplays);
    if (screen_get_context_property_pv(context, SCREEN_PROPERTY_DISPLAYS,
                                       (void **)displays) == -1) {
        perror("screen_get_context_property_pv(displays)");
        return 1;
    }

    printf("displays[0]=%p\n", displays[0]);

    // Create a screen event to inject keys.
    screen_event_t scr_event;
    if (screen_create_event(&scr_event) == -1) {
        perror("screen_create_event");
        return 1;
    }

    int type = SCREEN_EVENT_KEYBOARD;
    if (screen_set_event_property_iv(scr_event, SCREEN_PROPERTY_TYPE, &type)
        == -1) {
        perror("screen_set_event_property_iv(type)");
        return 1;
    }

    // Map the GPIO registers.
    gpio_regs = mmap(0, __PAGESIZE, PROT_READ | PROT_WRITE | PROT_NOCACHE,
                     MAP_PHYS | MAP_SHARED, NOFD, base_paddr + 0x200000);
    if (gpio_regs == MAP_FAILED) {
        fprintf(stderr, "%s: failed to map GPIO registers\n", __func__);
        return -1;
    }

    // Clear and disable all events.
    gpio_regs[REG_GPEDS0] = 0xffffffff;
    gpio_regs[REG_GPEDS1] = 0xffffffff;
    gpio_regs[REG_GPREN0] = 0;
    gpio_regs[REG_GPREN1] = 0;
    gpio_regs[REG_GPFEN0] = 0;
    gpio_regs[REG_GPFEN1] = 0;
    gpio_regs[REG_GPHEN0] = 0;
    gpio_regs[REG_GPHEN1] = 0;
    gpio_regs[REG_GPLEN0] = 0;
    gpio_regs[REG_GPLEN1] = 0;

    // Set GPIOs to input and detect rising edges.
    gpio_set_select(17, 0);
    gpio_detect_rising_edge(17);
    gpio_detect_falling_edge(17);

    struct sigevent event = {
        .sigev_notify = SIGEV_INTR,
    };

    // Attach to GPIO interrupt.
    int intr_id = InterruptAttachEvent(RPI_GPIO_INTR, &event, 0);
    if (intr_id == -1) {
        fprintf(stderr, "%s: failed to attach event interrupt\n", __func__);
        return -1;
    }
    InterruptUnmask(RPI_GPIO_INTR, intr_id);

    for (;;) {
        // Wait for an interrupt.
        InterruptWait(0, 0);

        // Clear events.
        gpio_regs[REG_GPEDS0] = 0xffffffff;

        // Check the button status.
        int flags = KEY_SYM_VALID;
        if (gpio_read(17) == 1) {
            flags |= KEY_DOWN;
        }

        if (screen_set_event_property_iv(scr_event, SCREEN_PROPERTY_FLAGS,
                                         &flags)
            == -1) {
            perror("screen_set_event_property_iv(flags)");
            return 1;
        }

        int sym = KEYCODE_DOWN;
        if (screen_set_event_property_iv(scr_event, SCREEN_PROPERTY_SYM,
                                         &sym)
            == -1) {
            perror("screen_set_event_property_iv(key_cap)");
            return 1;
        }

        // Send the event.
        printf("Inject sym=%x flags=%x\n", sym, flags);
        if (screen_inject_event(displays[0], scr_event) == -1) {
            perror("screen_inject_event");
            return 1;
        }

        InterruptUnmask(RPI_GPIO_INTR, intr_id);
    }

    return 0;
}
