/*
 *  GRUB  --  GRand Unified Bootloader
 *
 *  Copyright (C) 2012  Google Inc.
 *  Copyright (C) 2016  Free Software Foundation, Inc.
 *
 *  This is based on depthcharge code.
 *
 *  GRUB is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <grub/mm.h>
#include <grub/time.h>
#include <grub/misc.h>
#include <grub/arm/cros_ec.h>
#include <grub/fdtbus.h>

static const grub_uint64_t FRAMING_TIMEOUT_MS = 300;

static const grub_uint8_t EC_FRAMING_BYTE = 0xec;

#define EC_CMD_MKBP_STATE 0x60
#define EC_CMD_VERSION0 0xdc

static grub_uint64_t last_transfer;

static void
stop_bus (const struct grub_fdtbus_dev *spi)
{
  spi->driver->stop (spi);
  last_transfer = grub_get_time_ms ();
}

static int
wait_for_frame (const struct grub_fdtbus_dev *spi)
{
  grub_uint64_t start = grub_get_time_ms ();
  grub_uint8_t byte;
  do
    {
      if (spi->driver->receive (spi, &byte, 1))
	return -1;
      if (byte != EC_FRAMING_BYTE &&
	  grub_get_time_ms () - start > FRAMING_TIMEOUT_MS)
	{
	  grub_dprintf ("cros", "Timeout waiting for framing byte.\n");
	  return -1;
	}
    }
  while (byte != EC_FRAMING_BYTE);
  return 0;
}

/*
 * Calculate a simple 8-bit checksum of a data block
 *
 * @param data	Data block to checksum
 * @param size	Size of data block in bytes
 * @return checksum value (0 to 255)
 */
static grub_uint8_t
cros_ec_calc_checksum (const void *data, int size)
{
  grub_uint8_t csum;
  const grub_uint8_t *bytes = data;
  int i;

  for (i = csum = 0; i < size; i++)
    csum += bytes[i];
  return csum & 0xff;
}

enum
{
  /* response, arglen */
  CROS_EC_SPI_IN_HDR_SIZE = 2,
  /* version, cmd, arglen */
  CROS_EC_SPI_OUT_HDR_SIZE = 3
};

static grub_uint8_t busbuf[256];
#define MSG_BYTES ((int)sizeof (busbuf))

static int
ec_command (const struct grub_fdtbus_dev *dev, int cmd, int cmd_version,
	    const void *dout, int dout_len, void *din, int din_len)
{
  const struct grub_fdtbus_dev *spi = dev->parent;
  grub_uint8_t *bytes;

  /* Header + data + checksum. */
  grub_uint32_t out_bytes = CROS_EC_SPI_OUT_HDR_SIZE + dout_len + 1;
  grub_uint32_t in_bytes = CROS_EC_SPI_IN_HDR_SIZE + din_len + 1;

  /*
   * Sanity-check I/O sizes given transaction overhead in internal
   * buffers.
   */
  if (out_bytes > MSG_BYTES)
    {
      grub_dprintf ("cros", "Cannot send %d bytes\n", dout_len);
      return -1;
    }
  if (in_bytes > MSG_BYTES)
    {
      grub_dprintf ("cros", "Cannot receive %d bytes\n", din_len);
      return -1;
    }

  /* Prepare the output. */
  bytes = busbuf;
  *bytes++ = EC_CMD_VERSION0 + cmd_version;
  *bytes++ = cmd;
  *bytes++ = dout_len;
  grub_memcpy (bytes, dout, dout_len);
  bytes += dout_len;

  *bytes++ = cros_ec_calc_checksum (busbuf,
				    CROS_EC_SPI_OUT_HDR_SIZE + dout_len);

  /* Depthcharge uses 200 us here but GRUB timer resolution is only 1ms,
     decrease this when we increase timer resolution.  */
  while (grub_get_time_ms () - last_transfer < 1)
    ;

  if (spi->driver->start (spi))
    return -1;

  /* Allow EC to ramp up clock after being awoken. */
  /* Depthcharge only waits 100 us here but GRUB timer resolution is only 1ms,
     decrease this when we increase timer resolution.  */
  grub_millisleep (1);

  if (spi->driver->send (spi, busbuf, out_bytes))
    {
      stop_bus (spi);
      return -1;
    }

  /* Wait until the EC is ready. */
  if (wait_for_frame (spi))
    {
      stop_bus (spi);
      return -1;
    }

  /* Read the response code and the data length. */
  bytes = busbuf;
  if (spi->driver->receive (spi, bytes, 2))
    {
      stop_bus (spi);
      return -1;
    }
  grub_uint8_t result = *bytes++;
  grub_uint8_t length = *bytes++;

  /* Make sure there's enough room for the data. */
  if (CROS_EC_SPI_IN_HDR_SIZE + length + 1 > MSG_BYTES)
    {
      grub_dprintf ("cros", "Received length %#02x too large\n", length);
      stop_bus (spi);
      return -1;
    }

  /* Read the data and the checksum, and finish up. */
  if (spi->driver->receive (spi, bytes, length + 1))
    {
      stop_bus (spi);
      return -1;
    }
  bytes += length;
  int expected = *bytes++;
  stop_bus (spi);

  /* Check the integrity of the response. */
  if (result != 0)
    {
      grub_dprintf ("cros", "Received bad result code %d\n", result);
      return -result;
    }

  int csum = cros_ec_calc_checksum (busbuf,
				    CROS_EC_SPI_IN_HDR_SIZE + length);

  if (csum != expected)
    {
      grub_dprintf ("cros", "Invalid checksum rx %#02x, calced %#02x\n",
		    expected, csum);
      return -1;
    }

  /* If the caller wants the response, copy it out for them. */
  if (length < din_len)
    din_len = length;
  if (din)
    {
      grub_memcpy (din, (grub_uint8_t *) busbuf + CROS_EC_SPI_IN_HDR_SIZE, din_len);
    }

  return din_len;
}

int
grub_cros_ec_scan_keyboard (const struct grub_fdtbus_dev *dev, struct grub_cros_ec_keyscan *scan)
{
  if (ec_command (dev, EC_CMD_MKBP_STATE, 0, NULL, 0, scan,
		  sizeof (*scan)) < (int) sizeof (*scan))
    return -1;

  return 0;
}

int
grub_cros_ec_validate (const struct grub_fdtbus_dev *dev)
{
  if (!grub_fdtbus_is_compatible("google,cros-ec-spi", dev))
    return 0;
  if (!dev->parent)
    return 0;
  if (!dev->parent->driver)
    return 0;
  if (!dev->parent->driver->send
      || !dev->parent->driver->receive)
    return 0;
  return 1;
}