#include <errno.h>
#include <fcntl.h>

#include "librespot-c-internal.h"

/* -------------------------------- Channels -------------------------------- */

/*
Here is my current understanding of the channel concept:

1. A channel is established for retrieving chunks of audio. A channel is not a
   separate connection, all the traffic goes via the same Shannon-encrypted tcp
   connection as the rest.
2. It depends on the cmd whether a channel is used. CmdStreamChunk,
   CmdStreamChunkRes, CmdChannelError, CmdChannelAbort use channels. A channel
   is identified with a uint16_t, which is the first 2 bytes of these packets.
3. A channel is established with CmdStreamChunk where receiver picks channel id.
   Spotify responds with CmdStreamChunkRes that initially has some headers after
   the channel id. The headers are "reverse tlv": uint16_t header length,
   uint8_t header id, uint8_t header_data[]. The length includes the id length.
4. After the headers are sent the channel switches to data mode. This is
   signalled by a header length of 0. In data mode Spotify sends the requested
   chunks of audio (CmdStreamChunkRes) which have the audio right after the
   channel id prefix. The audio is AES encrypted with a per-file key. An empty
   CmdStreamChunkRes indicates the end. The caller can then make a new
   CmdStreamChunk requesting the next data.
5. For Ogg, the first 167 bytes of audio is a special Spotify header.
6. The channel can presumably be reset with CmdChannelAbort (?)
*/

static int
path_to_media_id_and_type(struct sp_file *file)
{
  char *ptr;

  file->media_type = SP_MEDIA_UNKNOWN;
  if (strstr(file->path, ":track:"))
    file->media_type = SP_MEDIA_TRACK;
  else if (strstr(file->path, ":episode:"))
    file->media_type = SP_MEDIA_EPISODE;
  else
    return -1;

  ptr = strrchr(file->path, ':');
  if (!ptr || strlen(ptr + 1) != 22)
    return -1;

  return crypto_base62_to_bin(file->media_id, sizeof(file->media_id), ptr + 1);
}

struct sp_channel *
channel_get(uint32_t channel_id, struct sp_session *session)
{
  if (channel_id > sizeof(session->channels)/sizeof(session->channels)[0])
    return NULL;

  if (!session->channels[channel_id].is_allocated)
    return NULL;

  return &session->channels[channel_id];
}

void
channel_free(struct sp_channel *channel)
{
  if (!channel || !channel->is_allocated)
    return;

  if (channel->audio_buf)
    evbuffer_free(channel->audio_buf);

  if (channel->audio_write_ev)
    event_free(channel->audio_write_ev);

  if (channel->audio_fd[0] >= 0)
    close(channel->audio_fd[0]);

  if (channel->audio_fd[1] >= 0)
    close(channel->audio_fd[1]);

  crypto_aes_free(&channel->file.decrypt);

  free(channel->file.path);

  memset(channel, 0, sizeof(struct sp_channel));

  channel->audio_fd[0] = -1;
  channel->audio_fd[1] = -1;
}

void
channel_free_all(struct sp_session *session)
{
  int i;

  for (i = 0; i < sizeof(session->channels)/sizeof(session->channels)[0]; i++)
    channel_free(&session->channels[i]);
}

int
channel_new(struct sp_channel **new_channel, struct sp_session *session, const char *path, struct event_base *evbase, event_callback_fn write_cb)
{
  struct sp_channel *channel;
  uint16_t i = SP_DEFAULT_CHANNEL;
  int ret;

  channel = &session->channels[i];

  channel_free(channel);
  channel->id = i;
  channel->is_allocated = true;

  channel->file.path = strdup(path);
  path_to_media_id_and_type(&channel->file);

  // Set up the audio I/O
  ret = pipe(channel->audio_fd);
  if (ret < 0)
    goto error;

  if (fcntl(channel->audio_fd[0], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0)
    goto error;

  if (fcntl(channel->audio_fd[1], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0)
    goto error;

  channel->audio_write_ev = event_new(evbase, channel->audio_fd[1], EV_WRITE, write_cb, session);
  if (!channel->audio_write_ev)
    goto error;

  channel->audio_buf = evbuffer_new();
  if (!channel->audio_buf)
    goto error;

  *new_channel = channel;

  return 0;

 error:
  channel_free(channel);
  return -1;
}

// Set the fd to non-blocking in case the caller changed that, and then read
// until empty
static int
channel_flush(int fd)
{
  uint8_t buf[4096];
  int flags;
  int got;

  flags = fcntl(fd, F_GETFL, 0);
  if (flags == -1)
    return -1;

  fcntl(fd, F_SETFL, flags | O_NONBLOCK);

  do
    got = read(fd, buf, sizeof(buf));
  while (got > 0);

  fcntl(fd, F_SETFL, flags);
  return 0;
}

void
channel_play(struct sp_channel *channel)
{
  channel->is_writing = true;
}

void
channel_stop(struct sp_channel *channel)
{
  channel->is_writing = false;

  // This will tell the reader that there is no more to read. He should then
  // call librespotc_close(), which will clean up the rest of the channel via
  // channel_free().
  close(channel->audio_fd[1]);
  channel->audio_fd[1] = -1;
}

int
channel_seek(struct sp_channel *channel, size_t pos)
{
  uint32_t seek_words;
  int ret;

  ret = channel_flush(channel->audio_fd[0]);
  if (ret < 0)
    RETURN_ERROR(SP_ERR_INVALID, "Could not flush read fd before seeking");

  channel->seek_pos = pos;

  // If seek + header isn't word aligned we will get up to 3 bytes before the
  // actual seek position. We will remove those when they are received.
  channel->seek_align = (pos + SP_OGG_HEADER_LEN) % 4;

  seek_words = (pos + SP_OGG_HEADER_LEN) / 4;

  ret = crypto_aes_seek(&channel->file.decrypt, 4 * seek_words, &sp_errmsg);
  if (ret < 0)
    RETURN_ERROR(SP_ERR_DECRYPTION, sp_errmsg);

  // Set the offset and received counter to match the seek
  channel->file.offset_words = seek_words;
  channel->file.received_words = seek_words;

  return 0;

 error:
  return ret;
}

void
channel_pause(struct sp_channel *channel)
{
  channel_flush(channel->audio_fd[0]);

  channel->is_writing = false;
}

// Always returns number of byte read so caller can advance read pointer. If
// header->len == 0 is returned it means that there are no more headers, and
// caller should switch the channel to data mode.
static ssize_t
channel_header_parse(struct sp_channel_header *header, uint8_t *data, size_t data_len)
{
  uint8_t *ptr;
  uint16_t be;

  if (data_len < sizeof(be))
    return -1;

  ptr = data;
  memset(header, 0, sizeof(struct sp_channel_header));

  memcpy(&be, ptr, sizeof(be));
  header->len = be16toh(be);
  ptr += sizeof(be);

  if (header->len == 0)
    goto done; // No more headers
  else if (data_len < header->len + sizeof(be))
    return -1;

  header->id = ptr[0];
  ptr += 1;

  header->data = ptr;
  header->data_len = header->len - 1;
  ptr += header->data_len;

  assert(ptr - data == header->len + sizeof(be));

 done:
  return header->len + sizeof(be);
}

static void
channel_header_handle(struct sp_channel *channel, struct sp_channel_header *header)
{
  uint32_t be32;

  sp_cb.hexdump("Received header\n", header->data, header->data_len);

  // The only header that librespot seems to use is 0x3, which is the audio file
  // size in words (incl. headers?)
  if (header->id == 0x3)
    {
      if (header->data_len != sizeof(be32))
	{
	  sp_cb.logmsg("Unexpected header length for header id 0x3\n");
	  return;
	}

      memcpy(&be32, header->data, sizeof(be32));
      channel->file.len_words = be32toh(be32);
    }
}

static ssize_t
channel_header_trailer_read(struct sp_channel *channel, uint8_t *msg, size_t msg_len, struct sp_session *session)
{
  ssize_t parsed_len;
  ssize_t consumed_len;
  int ret;

  channel->file.end_of_chunk = false;
  channel->file.end_of_file = false;

  if (msg_len == 0)
    {
      channel->file.end_of_chunk = true;
      channel->file.end_of_file = (channel->file.received_words >= channel->file.len_words);

      // In preparation for next chunk
      channel->file.offset_words += SP_CHUNK_LEN_WORDS;
      channel->is_data_mode = false;

      return 0;
    }
  else if (channel->is_data_mode)
    {
      return 0;
    }

  for (consumed_len = 0; msg_len > 0; msg += parsed_len, msg_len -= parsed_len)
    {
      parsed_len = channel_header_parse(&channel->header, msg, msg_len);
      if (parsed_len < 0)
	RETURN_ERROR(SP_ERR_INVALID, "Invalid channel header");

      consumed_len += parsed_len;

      if (channel->header.len == 0)
	{
	  channel->is_data_mode = true;
	  break; // All headers read
	}

      channel_header_handle(channel, &channel->header);
    }

  return consumed_len;

 error:
  return ret;
}

static ssize_t
channel_data_read(struct sp_channel *channel, uint8_t *msg, size_t msg_len, struct sp_session *session)
{
  const char *errmsg;
  int ret;

  assert (msg_len % 4 == 0);

  channel->file.received_words += msg_len / 4;

  ret = crypto_aes_decrypt(msg, msg_len, &channel->file.decrypt, &errmsg);
  if (ret < 0)
    RETURN_ERROR(SP_ERR_DECRYPTION, errmsg);

  // Skip Spotify header
  // TODO What to do here when seeking
  if (!channel->is_spotify_header_received)
    {
      if (msg_len < SP_OGG_HEADER_LEN)
	RETURN_ERROR(SP_ERR_INVALID, "Invalid data received");

      channel->is_spotify_header_received = true;

      msg += SP_OGG_HEADER_LEN;
      msg_len -= SP_OGG_HEADER_LEN;
    }

  // See explanation of this in channel_seek()
  if (channel->seek_align)
    {
      msg += channel->seek_align;
      msg_len -= channel->seek_align;
      channel->seek_align = 0;
    }

  channel->body.data = msg;
  channel->body.data_len = msg_len;

  return 0;

 error:
  return ret;
}

int
channel_data_write(struct sp_channel *channel)
{
  ssize_t wrote;
  int ret;

  wrote = evbuffer_write(channel->audio_buf, channel->audio_fd[1]);
  if (wrote < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
    return SP_OK_WAIT;
  else if (wrote < 0)
    RETURN_ERROR(SP_ERR_WRITE, "Error writing to audio pipe");

  channel->audio_written_len += wrote;

  if (evbuffer_get_length(channel->audio_buf) > 0)
    return SP_OK_WAIT;

  return SP_OK_DONE;

 error:
  return ret;
}

int
channel_msg_read(uint16_t *channel_id, uint8_t *msg, size_t msg_len, struct sp_session *session)
{
  struct sp_channel *channel;
  uint16_t be;
  ssize_t consumed_len;
  int ret;

  if (msg_len < sizeof(be))
    RETURN_ERROR(SP_ERR_INVALID, "Chunk response is too small");

  memcpy(&be, msg, sizeof(be));
  *channel_id = be16toh(be);

  channel = channel_get(*channel_id, session);
  if (!channel)
    {
      sp_cb.hexdump("Message with unknown channel\n", msg, msg_len);
      RETURN_ERROR(SP_ERR_INVALID, "Could not recognize channel in chunk response");
    }

  msg += sizeof(be);
  msg_len -= sizeof(be);

  // Will set data_mode, end_of_file and end_of_chunk as appropriate
  consumed_len = channel_header_trailer_read(channel, msg, msg_len, session);
  if (consumed_len < 0)
    RETURN_ERROR((int)consumed_len, sp_errmsg);

  msg += consumed_len;
  msg_len -= consumed_len;

  channel->body.data = NULL;
  channel->body.data_len = 0;

  if (!channel->is_data_mode || !(msg_len > 0))
    return 0; // Not in data mode or no data to read

  consumed_len = channel_data_read(channel, msg, msg_len, session);
  if (consumed_len < 0)
    RETURN_ERROR((int)consumed_len, sp_errmsg);

  return 0;

 error:
  return ret;
}

