/*
 * Copyright 2018 Google Inc.
 *
 * Licensed 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.
 */

// Pim 2018-04-03: This is a Linux implementation of the mgos_i2c interface
// which is useful to develop drivers directly on Linux. For Ubuntu/Debian,
// make sure to install package 'libi2c-dev'.

#include "mgos_i2c.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <i2c/smbus.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <errno.h>

struct mgos_i2c
{
  int fd;
  uint16_t read_timeout_ms;      // in msec
  char *filename;
};

static struct mgos_i2c *s_global_i2c_bus = NULL;

static size_t i2c_read_timeout(struct mgos_i2c *i2c, void *data, size_t len) {
  uint16_t tries;
  size_t ret=-1;

  if (!i2c) return -1;

  ret = read(i2c->fd, data, len);

  for(tries=i2c->read_timeout_ms; tries && ret!=len && len>0; tries--) {
    usleep(1000);
    ret = read(i2c->fd, data, len);
  }
  if (ret!=len) {
    //    LOG(LL_ERROR, ("Read timeout on I2C"));
    return -1;
  }
  return len;
}


bool mgos_i2c_read(struct mgos_i2c *i2c, uint16_t addr, void *data, size_t len, bool stop) {
  size_t ret;

  if (!i2c) {
    LOG(LL_ERROR, ("No I2C bus"));
    return false;
  }

  if (ioctl(i2c->fd,I2C_SLAVE,addr) < 0) {
    LOG(LL_ERROR, ("Cannot select slave 0x%04x on I2C bus", addr));
    return false;
  }
  ret = i2c_read_timeout(i2c, data, len);
  if (ret != len) {
    //    LOG(LL_DEBUG, ("RECV %ld bytes (wanted %lu) from 0x%02x: %s", ret, len, addr, strerror(errno)));
    return false;
  }
  return true;
  (void)stop;
}


bool mgos_i2c_write(struct mgos_i2c *i2c, uint16_t addr, const void *data, size_t len, bool stop) {
  size_t ret;

  if (!i2c) {
    LOG(LL_ERROR, ("No I2C bus"));
    return false;
  }

  if (ioctl(i2c->fd,I2C_SLAVE,addr) < 0) {
    LOG(LL_ERROR, ("Cannot select slave 0x%04x on I2C bus", addr));
    return false;
  }
  ret = write(i2c->fd, data, len);
  if (ret != len) {
    //    LOG(LL_DEBUG, ("XMIT %ld bytes (wanted %lu) from 0x%02x: %s", ret, len, addr, strerror(errno)));
    return false;
  }
  return true;
  (void)stop;
}


void mgos_i2c_stop(struct mgos_i2c *i2c) {
  return;
  (void)i2c;
}


int mgos_i2c_get_freq(struct mgos_i2c *i2c) {
  return MGOS_I2C_FREQ_100KHZ;
  (void)i2c;
}


bool mgos_i2c_set_freq(struct mgos_i2c *i2c, int freq) {
  if (freq==MGOS_I2C_FREQ_100KHZ) return true;
  return false;
  (void)i2c;
}


int mgos_i2c_read_reg_b(struct mgos_i2c *i2c, uint16_t addr, uint8_t reg) {
  uint8_t value;

  if (!mgos_i2c_read_reg_n(i2c, addr, reg, 1, &value))
    return -1;
  return value;
}


int mgos_i2c_read_reg_w(struct mgos_i2c *i2c, uint16_t addr, uint8_t reg) {
  uint16_t value;
  uint8_t data[2];

  if (!mgos_i2c_read_reg_n(i2c, addr, reg, 2, data))
    return -1;
  value=(data[0]<<8)+data[1];
  return value;
}


bool mgos_i2c_read_reg_n(struct mgos_i2c *i2c, uint16_t addr, uint8_t reg, size_t n, uint8_t *buf) {
  uint8_t outbuf;
  struct i2c_rdwr_ioctl_data packets;
  struct i2c_msg messages[2];

  if (!i2c) {
    LOG(LL_ERROR, ("No I2C bus"));
    return -1;
  }

  if (ioctl(i2c->fd,I2C_SLAVE,addr) < 0) {
    LOG(LL_ERROR, ("Cannot select slave 0x%04x on I2C bus: %s", addr, strerror(errno)));
    return -1;
  }
  /*
   * In order to read a register, we first do a "dummy write" by writing
   * 0 bytes to the register we want to read from.  This is similar to
   * the packet in set_i2c_register, except it's 1 byte rather than 2.
   */
  outbuf = reg;
  messages[0].addr  = addr;
  messages[0].flags = 0;
  messages[0].len   = sizeof(outbuf);
  messages[0].buf   = &outbuf;

  /* The data will get returned in this structure */
  messages[1].addr  = addr;
  messages[1].flags = I2C_M_RD/* | I2C_M_NOSTART*/;
  messages[1].len   = n;
  messages[1].buf   = buf;

  /* Send the request to the kernel and get the result back */
  packets.msgs      = messages;
  packets.nmsgs     = 2;
  if(ioctl(i2c->fd, I2C_RDWR, &packets) < 0)
    return false;

  return true;
}


bool mgos_i2c_write_reg_w(struct mgos_i2c *i2c, uint16_t addr, uint8_t reg, uint16_t value) {
  uint8_t data[2];
  data[0]=value>>8;
  data[1]=value&0xFF;

  return mgos_i2c_write_reg_n(i2c, addr, reg, 2, data);
}


bool mgos_i2c_write_reg_b(struct mgos_i2c *i2c, uint16_t addr, uint8_t reg, uint8_t value) {
  return mgos_i2c_write_reg_n(i2c, addr, reg, 1, &value);
}


bool mgos_i2c_write_reg_n(struct mgos_i2c *i2c, uint16_t addr, uint8_t reg, size_t n, const uint8_t *buf) {
  unsigned char outbuf[n+1];
  struct i2c_rdwr_ioctl_data packets;
  struct i2c_msg messages[1];

  if (!i2c) {
    LOG(LL_ERROR, ("No I2C bus"));
    return false;
  }

  if (ioctl(i2c->fd,I2C_SLAVE,addr) < 0) {
    LOG(LL_ERROR, ("Cannot select slave 0x%04x on I2C bus: %s", addr, strerror(errno)));
    return false;
  }

  messages[0].addr=addr;
  messages[0].flags=0;
  messages[0].len=sizeof(outbuf);
  messages[0].buf=outbuf;

  outbuf[0]=reg;
  memcpy(outbuf+1, buf, n);

  packets.msgs  = messages;
  packets.nmsgs = 1;
  if(ioctl(i2c->fd, I2C_RDWR, &packets) < 0) {
    return false;
  }
  return true;
}


void mgos_i2c_close(struct mgos_i2c *i2c) {
  return;
  (void)i2c;
}


struct mgos_i2c *mgos_i2c_get_global(void) {
  return s_global_i2c_bus;
}


// User provided function to interface with Linux I2C driver
bool mgos_i2c_open(int busnr) {
  int fd;
  char filename[20];
  struct mgos_i2c *i2c;

  sprintf(filename,"/dev/i2c-%d",busnr);
  if ((fd = open(filename,O_RDWR)) < 0) {
    LOG(LL_ERROR, ("Could not open %s", filename));
    return false;
  }
  i2c = calloc(1, sizeof(struct mgos_i2c));
  if (!i2c) {
    LOG(LL_ERROR, ("Could not allocate mgos_i2c handle for %s", filename));
    return false;
  }
  i2c->fd=fd;
  i2c->filename=strdup(filename);
  i2c->read_timeout_ms=1000;
  s_global_i2c_bus=i2c;
  LOG(LL_INFO, ("Opened I2C bus on %s", filename));

  return true;
}
