/*
 * Copyright (c) 2022, Erich Styger
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

/* see
 * https://blog.lxsang.me/post/id/33
 * https://embeddedbits.org/new-linux-kernel-gpio-user-space-interface/
 * https://www.ics.com/blog/gpio-programming-exploring-libgpiod-library
 * https://lloydrochester.com/post/hardware/libgpiod-intro-rpi/
 *  */

#include "gpio.h"

/* include API header for the new interface */
#include <linux/gpio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdint.h>
#include <getopt.h>
#include <stdlib.h>
#include <sys/poll.h>

#define GPIO_DEV_NAME  "/dev/gpiochip0"

/* HAT v7 requires pull-ups enabled. In case if using older headers, define locally the new flags */
#ifndef GPIOHANDLE_REQUEST_BIAS_PULL_UP /* supported with latest libs: if not defined, maybe using older header files? Let's assume the library is a recent one */
  #define GPIOHANDLE_REQUEST_BIAS_PULL_UP       (1UL << 5)
  #define GPIOHANDLE_REQUEST_BIAS_PULL_DOWN     (1UL << 6)
  #define GPIOHANDLE_REQUEST_BIAS_DISABLE       (1UL << 7)
#endif

typedef struct {
  struct gpiohandle_request rq; /* request data structure */
  struct gpiohandle_data data;
} Gpio_Desc_t;

static Gpio_Desc_t yellowLED, redLED, greenLED;
#if PL_HAT_VERSION<=6
  static Gpio_Desc_t blueLED;
#endif
static Gpio_Desc_t upBTN, downBTN, leftBTN, rightBTN, centerBTN;

static void gpio_list(const char *dev_name) {
  struct gpiochip_info info;
  struct gpioline_info line_info;
  int fd, ret;

  fd = open(dev_name, O_RDONLY);
  if (fd < 0) {
    printf("Unabled to open %s: %s", dev_name, strerror(errno));
    return;
  }
  ret = ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &info);
  if (ret == -1) {
    printf("Unable to get chip info from ioctl: %s", strerror(errno));
    close(fd);
    return;
  }
  printf("Chip name: %s\n", info.name);
  printf("Chip label: %s\n", info.label);
  printf("Number of lines: %d\n", info.lines);
  for (int i = 0; i < info.lines; i++) {
    line_info.line_offset = i;
    ret = ioctl(fd, GPIO_GET_LINEINFO_IOCTL, &line_info);
    if (ret == -1) {
      printf("Unable to get line info from offset %d: %s", i, strerror(errno));
    } else {
      printf("offset: %d, name: %s, consumer: %s. Flags:\t[%s]\t[%s]\t[%s]\t[%s]\t[%s]\t[%s]\t[%s]\t[%s]\n",
         i,
         line_info.name,
         line_info.consumer,
         (line_info.flags & GPIOLINE_FLAG_IS_OUT) ? "OUTPUT" : "INPUT",
         (line_info.flags & GPIOLINE_FLAG_ACTIVE_LOW) ? "ACTIVE_LOW" : "ACTIVE_HIGHT",
         (line_info.flags & GPIOLINE_FLAG_OPEN_DRAIN) ? "OPEN_DRAIN" : "...",
         (line_info.flags & GPIOHANDLE_REQUEST_BIAS_PULL_UP) ? "PULL-UP" : "...",
         (line_info.flags & GPIOHANDLE_REQUEST_BIAS_PULL_DOWN) ? "PULL-DOWN" : "...",
         (line_info.flags & GPIOHANDLE_REQUEST_BIAS_DISABLE) ? "PULL-DISABLE" : "...",
         (line_info.flags & GPIOLINE_FLAG_OPEN_SOURCE) ? "OPENSOURCE" : "...",
         (line_info.flags & GPIOLINE_FLAG_KERNEL) ? "KERNEL" : "");
    }
  }
  close(fd);
}

static void gpio_write(const char *dev_name, int offset, uint8_t value) {
  struct gpiohandle_request rq;
  struct gpiohandle_data data;
  int fd, ret;

  printf("Write value %d to GPIO at offset %d (OUTPUT mode) on chip %s\n", value, offset, dev_name);
  fd = open(dev_name, O_RDONLY);
  if (fd < 0) {
    printf("Unabled to open %s: %s", dev_name, strerror(errno));
    return;
  }
  rq.lineoffsets[0] = offset;
  rq.flags = GPIOHANDLE_REQUEST_OUTPUT;
  rq.lines = 1;
  ret = ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &rq);
  close(fd);
  if (ret == -1) {
    printf("Unable to line handle from ioctl : %s", strerror(errno));
    return;
  }
  data.values[0] = value;
  ret = ioctl(rq.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
  if (ret == -1) {
    printf("Unable to set line value using ioctl : %s", strerror(errno));
  } else {
    usleep(2000000); /* wait 2 sec */
  }
  close(rq.fd);
}

static void writePin(Gpio_Desc_t *io, uint8_t value) {
    int ret;

    io->data.values[0] = value;
    ret = ioctl(io->rq.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &io->data);
    if (ret == -1) {
        printf("Unable to set line value using ioctl : %s", strerror(errno));
    }
}

static bool readPin(Gpio_Desc_t *io) {
    int ret;
    bool result = false;

    ret = ioctl(io->rq.fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &io->data);
    if (ret == -1) {
        printf("Unable to get line value using ioctl : %s", strerror(errno));
        result = false;
    } else {
        result = io->data.values[0]!=0;
    }
    return result;
}

static int gpio_read(const char *dev_name, int offset)
{
    struct gpiohandle_request rq;
    struct gpiohandle_data data;
    int fd, ret;
    int result = -1;

    fd = open(dev_name, O_RDONLY);
    if (fd < 0)
    {
        printf("Unabled to open %s: %s", dev_name, strerror(errno));
        return -1;
    }
    rq.lineoffsets[0] = offset;
    rq.flags = GPIOHANDLE_REQUEST_INPUT;
    rq.lines = 1;
    ret = ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &rq);
    close(fd);
    if (ret == -1)
    {
        printf("Unable to get line handle from ioctl : %s", strerror(errno));
        return -1;
    }
    ret = ioctl(rq.fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &data);
    if (ret == -1)
    {
        printf("Unable to get line value using ioctl : %s", strerror(errno));
        result = -1;
    }
    else
    {
        printf("Value of GPIO at offset %d (INPUT mode) on chip %s: %d\n", offset, dev_name, data.values[0]);
        result = data.values[0];
    }
    close(rq.fd);
    return result;
}

static void gpio_poll(const char *dev_name, int offset)
{
    struct gpioevent_request rq;
    struct pollfd pfd;
    int fd, ret;

    fd = open(dev_name, O_RDONLY);
    if (fd < 0)
    {
        printf("Unabled to open %s: %s", dev_name, strerror(errno));
        return;
    }
    rq.lineoffset = offset;
    rq.eventflags = GPIOEVENT_EVENT_RISING_EDGE;
    ret = ioctl(fd, GPIO_GET_LINEEVENT_IOCTL, &rq);
    close(fd);
    if (ret == -1)
    {
        printf("Unable to get line event from ioctl : %s", strerror(errno));
        close(fd);
        return;
    }
    pfd.fd = rq.fd;
    pfd.events = POLLIN;
    ret = poll(&pfd, 1, -1);
    if (ret == -1)
    {
        printf("Error while polling event from GPIO: %s", strerror(errno));
    }
    else if (pfd.revents & POLLIN)
    {
        printf("Rising edge event on GPIO offset: %d, of %s\n", offset, dev_name);
    }
    close(rq.fd);
}

bool Gpio_ReadPin(Gpio_Pins_e pin) {
  bool val;
  //return gpio_read(GPIO_DEV_NAME, pin);
  switch(pin) {
    case Gpio_Pins_BTN_UP_BCM:
      val = readPin(&upBTN);
      break;
    case Gpio_Pins_BTN_DOWN_BCM:
      val = readPin(&downBTN);
      break;
    case Gpio_Pins_BTN_LEFT_BCM:
      val = readPin(&leftBTN);
      break;
    case Gpio_Pins_BTN_RIGHT_BCM:
      val = readPin(&rightBTN);
      break;
    case Gpio_Pins_BTN_CENTER_BCM:
      val = readPin(&centerBTN);
      break;
     default:
       val = false;
      break;
  }
  return val;
}

void Gpio_WritePin(Gpio_Pins_e pin, bool val) {
  switch(pin) {
#if PL_HAT_VERSION<=6
    case Gpio_Pins_LED_BLUE_PIN_BCM:
      writePin(&blueLED, val);
      break;
#endif
    case Gpio_Pins_LED_RED_PIN_BCM:
      writePin(&redLED, val);
      break;
    case Gpio_Pins_LED_GREEN_PIN_BCM:
      writePin(&greenLED, val);
      break;
      case Gpio_Pins_LED_YELLOW_PIN_BCM:
      writePin(&yellowLED, val);
      break;

    default:
      break;
  }
}

int Gpio_Deinit(void) {
#if PL_HAT_VERSION<=6
  close(blueLED.rq.fd);
#endif
  close(redLED.rq.fd);
  close(greenLED.rq.fd);
  close(yellowLED.rq.fd);

  close(upBTN.rq.fd);
  close(downBTN.rq.fd);
  close(leftBTN.rq.fd);
  close(rightBTN.rq.fd);
  close(centerBTN.rq.fd);
  return 0;
}

static int RequestPin(Gpio_Desc_t *desc, Gpio_Pins_e pin, int lineRequestFlags, const char *label) {
  int fd, ret;

  fd = open(GPIO_DEV_NAME, O_RDONLY);
  if (fd < 0) {
      printf("Unabled to open %s: %s", GPIO_DEV_NAME, strerror(errno));
      return -1;
  }
  desc->rq.lineoffsets[0] = pin;
  desc->rq.flags = lineRequestFlags;
  memcpy(desc->rq.default_values, &desc->data, sizeof(desc->rq.default_values));
  strcpy(desc->rq.consumer_label, label);
  desc->rq.lines = 1;
  ret = ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &desc->rq);
  close(fd);
  if (ret == -1) {
      printf("Unable to get line handle from ioctl : %s", strerror(errno));
      return -1;
  }
  return 0;
}

int Gpio_Init(void) {
#if GPIO_HAT_VERSION<=6
  if (RequestPin(&blueLED, Gpio_Pins_LED_BLUE_PIN_BCM, GPIOHANDLE_REQUEST_OUTPUT, "led_blue")!=0) {
    return -1;
  }
#endif
  if (RequestPin(&redLED, Gpio_Pins_LED_RED_PIN_BCM, GPIOHANDLE_REQUEST_OUTPUT, "led_red")!=0) {
    return -1;
  }
  if (RequestPin(&greenLED, Gpio_Pins_LED_GREEN_PIN_BCM, GPIOHANDLE_REQUEST_OUTPUT, "led_green")!=0) {
    return -1;
  }

  if (RequestPin(&yellowLED, Gpio_Pins_LED_YELLOW_PIN_BCM, GPIOHANDLE_REQUEST_OUTPUT, "led_yellow")!=0) {
    return -1;
  }

  int inputPinFlags = GPIOHANDLE_REQUEST_INPUT;
#if PL_HAT_VERSION>6
  inputPinFlags |= GPIOHANDLE_REQUEST_BIAS_PULL_UP;
#endif
  if (RequestPin(&upBTN, Gpio_Pins_BTN_UP_BCM, inputPinFlags, "button_up")!=0) {
    return -1;
  }
  if (RequestPin(&downBTN, Gpio_Pins_BTN_DOWN_BCM, inputPinFlags, "button_down")!=0) {
    return -1;
  }
  if (RequestPin(&leftBTN, Gpio_Pins_BTN_LEFT_BCM, inputPinFlags, "button_left")!=0) {
    return -1;
  }
  if (RequestPin(&rightBTN, Gpio_Pins_BTN_RIGHT_BCM, inputPinFlags, "button_right")!=0) {
    return -1;
  }
  if (RequestPin(&centerBTN, Gpio_Pins_BTN_CENTER_BCM, inputPinFlags, "button_center")!=0) {
    return -1;
  }
#if 0 /* testing only */
  gpio_list(GPIO_DEV_NAME);
#endif
  return 0;
}
