/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 *
 */

#include <proton/error.h>
#include <proton/object.h>

#ifndef __cplusplus
#include <stdbool.h>
#endif
#include <stddef.h>
#include <string.h>
#include <stdio.h>

#include "buffer.h"
#include "memory.h"
#include "util.h"

struct pn_buffer_t {
  size_t capacity;
  size_t start;
  size_t size;
  char *bytes;
};

PN_STRUCT_CLASSDEF(pn_buffer)

pn_buffer_t *pn_buffer(size_t capacity)
{
  pn_buffer_t *buf = (pn_buffer_t *) pni_mem_allocate(PN_CLASSCLASS(pn_buffer), sizeof(pn_buffer_t));
  if (buf != NULL) {
    buf->capacity = capacity;
    buf->start = 0;
    buf->size = 0;
    if (capacity > 0) {
        buf->bytes = (char *) pni_mem_suballocate(PN_CLASSCLASS(pn_buffer), buf, capacity);
        if (buf->bytes == NULL) {
            pni_mem_deallocate(PN_CLASSCLASS(pn_buffer), buf);
            buf = NULL;
        }
    }
    else {
        buf->bytes = NULL;
    }
  }
  return buf;
}

void pn_buffer_free(pn_buffer_t *buf)
{
  if (buf) {
    pni_mem_subdeallocate(PN_CLASSCLASS(pn_buffer), buf, buf->bytes);
    pni_mem_deallocate(PN_CLASSCLASS(pn_buffer), buf);
  }
}

size_t pn_buffer_size(pn_buffer_t *buf)
{
  return buf->size;
}

size_t pn_buffer_capacity(pn_buffer_t *buf)
{
  return buf->capacity;
}

size_t pn_buffer_available(pn_buffer_t *buf)
{
  return buf->capacity - buf->size;
}

static size_t pni_buffer_head(pn_buffer_t *buf)
{
  return buf->start;
}

static size_t pni_buffer_tail(pn_buffer_t *buf)
{
  size_t tail = buf->start + buf->size;
  if (tail >= buf->capacity)
    tail -= buf->capacity;
  return tail;
}

static bool pni_buffer_wrapped(pn_buffer_t *buf)
{
  return buf->size && pni_buffer_head(buf) >= pni_buffer_tail(buf);
}

static size_t pni_buffer_tail_space(pn_buffer_t *buf)
{
  if (pni_buffer_wrapped(buf)) {
    return pn_buffer_available(buf);
  } else {
    return buf->capacity - pni_buffer_tail(buf);
  }
}

static size_t pni_buffer_head_space(pn_buffer_t *buf)
{
  if (pni_buffer_wrapped(buf)) {
    return pn_buffer_available(buf);
  } else {
    return pni_buffer_head(buf);
  }
}

static size_t pni_buffer_head_size(pn_buffer_t *buf)
{
  if (pni_buffer_wrapped(buf)) {
    return buf->capacity - pni_buffer_head(buf);
  } else {
    return pni_buffer_tail(buf) - pni_buffer_head(buf);
  }
}

static size_t pni_buffer_tail_size(pn_buffer_t *buf)
{
  if (pni_buffer_wrapped(buf)) {
    return pni_buffer_tail(buf);
  } else {
    return 0;
  }
}

int pn_buffer_ensure(pn_buffer_t *buf, size_t size)
{
  size_t old_capacity = buf->capacity;
  size_t old_head = pni_buffer_head(buf);
  bool wrapped = pni_buffer_wrapped(buf);

  while (pn_buffer_available(buf) < size) {
    buf->capacity = 2*(buf->capacity ? buf->capacity : 16);
  }

  if (buf->capacity != old_capacity) {
    char* new_bytes = (char *) pni_mem_subreallocate(PN_CLASSCLASS(pn_buffer), buf, buf->bytes, buf->capacity);
    if (new_bytes) {
      buf->bytes = new_bytes;

      if (wrapped) {
          size_t n = old_capacity - old_head;
          memmove(buf->bytes + buf->capacity - n, buf->bytes + old_head, n);
          buf->start = buf->capacity - n;
      }
    }
  }

  return 0;
}

int pn_buffer_append(pn_buffer_t *buf, const char *bytes, size_t size)
{
  if (!size) return 0;
  int err = pn_buffer_ensure(buf, size);
  if (err) return err;

  size_t tail = pni_buffer_tail(buf);
  size_t tail_space = pni_buffer_tail_space(buf);
  size_t n = pn_min(tail_space, size);

  // Heuristic check for a strange usage in messenger
  if (bytes!=buf->bytes+tail) {
    memcpy(buf->bytes + tail, bytes, n);
    memcpy(buf->bytes, bytes + n, size - n);
  }
  buf->size += size;

  return 0;
}

int pn_buffer_prepend(pn_buffer_t *buf, const char *bytes, size_t size)
{
  int err = pn_buffer_ensure(buf, size);
  if (err) return err;

  size_t head = pni_buffer_head(buf);
  size_t head_space = pni_buffer_head_space(buf);
  size_t n = pn_min(head_space, size);

  memcpy(buf->bytes + head - n, bytes + size - n, n);
  memcpy(buf->bytes + buf->capacity - (size - n), bytes, size - n);

  if (buf->start >= size) {
    buf->start -= size;
  } else {
    buf->start = buf->capacity - (size - buf->start);
  }

  buf->size += size;

  return 0;
}

static size_t pni_buffer_index(pn_buffer_t *buf, size_t index)
{
  size_t result = buf->start + index;
  if (result >= buf->capacity) result -= buf->capacity;
  return result;
}

size_t pn_buffer_get(pn_buffer_t *buf, size_t offset, size_t size, char *dst)
{
  size = pn_min(size, buf->size);
  size_t start = pni_buffer_index(buf, offset);
  size_t stop = pni_buffer_index(buf, offset + size);

  if (size == 0) return 0;

  size_t sz1;
  size_t sz2;

  if (start >= stop) {
    sz1 = buf->capacity - start;
    sz2 = stop;
  } else {
    sz1 = stop - start;
    sz2 = 0;
  }

  memcpy(dst, buf->bytes + start, sz1);
  memcpy(dst + sz1, buf->bytes, sz2);

  return sz1 + sz2;
}

int pn_buffer_trim(pn_buffer_t *buf, size_t left, size_t right)
{
  if (left + right > buf->size) return PN_ARG_ERR;

  // In special case where we trim everything just clear buffer
  if (left + right == buf->size) {
    pn_buffer_clear(buf);
    return 0;
  }
  buf->start += left;
  if (buf->start >= buf->capacity)
    buf->start -= buf->capacity;

  buf->size -= left + right;

  return 0;
}

void pn_buffer_clear(pn_buffer_t *buf)
{
  buf->start = 0;
  buf->size = 0;
}

static void pn_buffer_rotate (pn_buffer_t *buf, size_t sz) {
  if (sz == 0) return;

  unsigned c = 0, v = 0;
  for (; c < buf->capacity; v++) {
    unsigned t = v, tp = v + sz;
    char tmp = buf->bytes[v];
    c++;
    while (tp != v) {
      buf->bytes[t] = buf->bytes[tp];
      t = tp;
      tp += sz;
      if (tp >= buf->capacity) tp -= buf->capacity;
      c++;
    }
    buf->bytes[t] = tmp;
  }
}

int pn_buffer_defrag(pn_buffer_t *buf)
{
  pn_buffer_rotate(buf, buf->start);
  buf->start = 0;
  return 0;
}

pn_bytes_t pn_buffer_bytes(pn_buffer_t *buf)
{
  if (!buf) {
    return (pn_bytes_t){0, NULL};
  }
  pn_buffer_defrag(buf);
  return (pn_bytes_t){.size=buf->size, .start=buf->bytes};
}

pn_rwbytes_t pn_buffer_memory(pn_buffer_t *buf)
{
  if (!buf) {
    return (pn_rwbytes_t){0, NULL};
  }
  pn_buffer_defrag(buf);
  return (pn_rwbytes_t){.size=buf->size, .start=buf->bytes};
}

pn_rwbytes_t pn_buffer_free_memory(pn_buffer_t *buf)
{
  if (!buf) {
    return (pn_rwbytes_t){0, NULL};
  }
  size_t free_size = buf->capacity-buf->size;
  if (buf->start == 0) {
    return (pn_rwbytes_t){.size=free_size, .start=buf->bytes};
  }
  // If free memory in one single blob don't need to defragment
  if (buf->start+buf->size > buf->capacity) {
    return (pn_rwbytes_t){.size=free_size, .start=buf->bytes+buf->start-free_size};
  }
  pn_buffer_defrag(buf);
  return (pn_rwbytes_t){.size=free_size, .start=buf->bytes+buf->start+buf->size};
}

int pn_buffer_quote(pn_buffer_t *buf, pn_string_t *str, size_t n)
{
  size_t hsize = pni_buffer_head_size(buf);
  size_t tsize = pni_buffer_tail_size(buf);
  if (hsize >= n) {
    pn_quote(str, buf->bytes + pni_buffer_head(buf), n);
    return 0;
  }
  pn_quote(str, buf->bytes + pni_buffer_head(buf), hsize);
  pn_quote(str, buf->bytes, pn_min(tsize, n-hsize));
  return 0;
}
