/* GStreamer
 * Copyright (C) 2012 Wim Taymans <wim.taymans at gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */
/**
 * SECTION:rtsp-address-pool
 * @short_description: A pool of network addresses
 * @see_also: #GstRTSPStream, #GstRTSPStreamTransport
 *
 * The #GstRTSPAddressPool is an object that maintains a collection of network
 * addresses. It is used to allocate server ports and server multicast addresses
 * but also to reserve client provided destination addresses.
 *
 * A range of addresses can be added with gst_rtsp_address_pool_add_range().
 * Both multicast and unicast addresses can be added.
 *
 * With gst_rtsp_address_pool_acquire_address() an unused address and port range
 * can be acquired from the pool. With gst_rtsp_address_pool_reserve_address() a
 * specific address can be retrieved. Both methods return a boxed
 * #GstRTSPAddress that should be freed with gst_rtsp_address_free() after
 * usage, which brings the address back into the pool.
 *
 * Last reviewed on 2013-07-16 (1.0.0)
 */

#include <string.h>
#include <gio/gio.h>

#include "rtsp-address-pool.h"

/**
 * gst_rtsp_address_copy:
 * @addr: a #GstRTSPAddress
 *
 * Make a copy of @addr.
 *
 * Returns: a copy of @addr.
 */
GstRTSPAddress *
gst_rtsp_address_copy (GstRTSPAddress * addr)
{
  GstRTSPAddress *copy;

  g_return_val_if_fail (addr != NULL, NULL);

  copy = g_slice_dup (GstRTSPAddress, addr);
  /* only release to the pool when the original is freed. It's a bit
   * weird but this will do for now as it avoid us to use refcounting. */
  copy->pool = NULL;
  copy->address = g_strdup (copy->address);

  return copy;
}

static void gst_rtsp_address_pool_release_address (GstRTSPAddressPool * pool,
    GstRTSPAddress * addr);

/**
 * gst_rtsp_address_free:
 * @addr: a #GstRTSPAddress
 *
 * Free @addr and releasing it back into the pool when owned by a
 * pool.
 */
void
gst_rtsp_address_free (GstRTSPAddress * addr)
{
  g_return_if_fail (addr != NULL);

  if (addr->pool) {
    /* unrefs the pool and sets it to NULL */
    gst_rtsp_address_pool_release_address (addr->pool, addr);
  }
  g_free (addr->address);
  g_slice_free (GstRTSPAddress, addr);
}

G_DEFINE_BOXED_TYPE (GstRTSPAddress, gst_rtsp_address,
    (GBoxedCopyFunc) gst_rtsp_address_copy,
    (GBoxedFreeFunc) gst_rtsp_address_free);

GST_DEBUG_CATEGORY_STATIC (rtsp_address_pool_debug);
#define GST_CAT_DEFAULT rtsp_address_pool_debug

#define GST_RTSP_ADDRESS_POOL_GET_PRIVATE(obj)  \
     (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GST_TYPE_RTSP_ADDRESS_POOL, GstRTSPAddressPoolPrivate))

struct _GstRTSPAddressPoolPrivate
{
  GMutex lock;                  /* protects everything in this struct */
  GList *addresses;
  GList *allocated;

  gboolean has_unicast_addresses;
};

#define ADDR_IS_IPV4(a)      ((a)->size == 4)
#define ADDR_IS_IPV6(a)      ((a)->size == 16)
#define ADDR_IS_EVEN_PORT(a) (((a)->port & 1) == 0)

typedef struct
{
  guint8 bytes[16];
  gsize size;
  guint16 port;
} Addr;

typedef struct
{
  Addr min;
  Addr max;
  guint8 ttl;
} AddrRange;

#define RANGE_IS_SINGLE(r) (memcmp ((r)->min.bytes, (r)->max.bytes, (r)->min.size) == 0)

#define gst_rtsp_address_pool_parent_class parent_class
G_DEFINE_TYPE (GstRTSPAddressPool, gst_rtsp_address_pool, G_TYPE_OBJECT);

static void gst_rtsp_address_pool_finalize (GObject * obj);

static void
gst_rtsp_address_pool_class_init (GstRTSPAddressPoolClass * klass)
{
  GObjectClass *gobject_class;

  gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->finalize = gst_rtsp_address_pool_finalize;

  g_type_class_add_private (klass, sizeof (GstRTSPAddressPoolPrivate));

  GST_DEBUG_CATEGORY_INIT (rtsp_address_pool_debug, "rtspaddresspool", 0,
      "GstRTSPAddressPool");
}

static void
gst_rtsp_address_pool_init (GstRTSPAddressPool * pool)
{
  pool->priv = GST_RTSP_ADDRESS_POOL_GET_PRIVATE (pool);

  g_mutex_init (&pool->priv->lock);
}

static void
free_range (AddrRange * range)
{
  g_slice_free (AddrRange, range);
}

static void
gst_rtsp_address_pool_finalize (GObject * obj)
{
  GstRTSPAddressPool *pool;

  pool = GST_RTSP_ADDRESS_POOL (obj);

  g_list_free_full (pool->priv->addresses, (GDestroyNotify) free_range);
  g_list_free_full (pool->priv->allocated, (GDestroyNotify) free_range);
  g_mutex_clear (&pool->priv->lock);

  G_OBJECT_CLASS (parent_class)->finalize (obj);
}

/**
 * gst_rtsp_address_pool_new:
 *
 * Make a new #GstRTSPAddressPool.
 *
 * Returns: a new #GstRTSPAddressPool
 */
GstRTSPAddressPool *
gst_rtsp_address_pool_new (void)
{
  GstRTSPAddressPool *pool;

  pool = g_object_new (GST_TYPE_RTSP_ADDRESS_POOL, NULL);

  return pool;
}

/**
 * gst_rtsp_address_pool_clear:
 * @pool: a #GstRTSPAddressPool
 *
 * Clear all addresses in @pool. There should be no outstanding
 * allocations.
 */
void
gst_rtsp_address_pool_clear (GstRTSPAddressPool * pool)
{
  GstRTSPAddressPoolPrivate *priv;

  g_return_if_fail (GST_IS_RTSP_ADDRESS_POOL (pool));
  g_return_if_fail (pool->priv->allocated == NULL);

  priv = pool->priv;

  g_mutex_lock (&priv->lock);
  g_list_free_full (priv->addresses, (GDestroyNotify) free_range);
  priv->addresses = NULL;
  g_mutex_unlock (&priv->lock);
}

static gboolean
fill_address (const gchar * address, guint16 port, Addr * addr,
    gboolean is_multicast)
{
  GInetAddress *inet;

  inet = g_inet_address_new_from_string (address);
  if (inet == NULL)
    return FALSE;

  if (is_multicast != g_inet_address_get_is_multicast (inet)) {
    g_object_unref (inet);
    return FALSE;
  }

  addr->size = g_inet_address_get_native_size (inet);
  memcpy (addr->bytes, g_inet_address_to_bytes (inet), addr->size);
  g_object_unref (inet);
  addr->port = port;

  return TRUE;
}

static gchar *
get_address_string (Addr * addr)
{
  gchar *res;
  GInetAddress *inet;

  inet = g_inet_address_new_from_bytes (addr->bytes,
      addr->size == 4 ? G_SOCKET_FAMILY_IPV4 : G_SOCKET_FAMILY_IPV6);
  res = g_inet_address_to_string (inet);
  g_object_unref (inet);

  return res;
}

/**
 * gst_rtsp_address_pool_add_range:
 * @pool: a #GstRTSPAddressPool
 * @min_address: a minimum address to add
 * @max_address: a maximum address to add
 * @min_port: the minimum port
 * @max_port: the maximum port
 * @ttl: a TTL or 0 for unicast addresses
 *
 * Adds the addresses from @min_addess to @max_address (inclusive)
 * to @pool. The valid port range for the addresses will be from @min_port to
 * @max_port inclusive.
 *
 * When @ttl is 0, @min_address and @max_address should be unicast addresses.
 * @min_address and @max_address can be set to
 * #GST_RTSP_ADDRESS_POOL_ANY_IPV4 or #GST_RTSP_ADDRESS_POOL_ANY_IPV6 to bind
 * to all available IPv4 or IPv6 addresses.
 *
 * When @ttl > 0, @min_address and @max_address should be multicast addresses.
 *
 * Returns: %TRUE if the addresses could be added.
 */
gboolean
gst_rtsp_address_pool_add_range (GstRTSPAddressPool * pool,
    const gchar * min_address, const gchar * max_address,
    guint16 min_port, guint16 max_port, guint8 ttl)
{
  AddrRange *range;
  GstRTSPAddressPoolPrivate *priv;
  gboolean is_multicast;

  g_return_val_if_fail (GST_IS_RTSP_ADDRESS_POOL (pool), FALSE);
  g_return_val_if_fail (min_port <= max_port, FALSE);

  priv = pool->priv;

  is_multicast = ttl != 0;

  range = g_slice_new0 (AddrRange);

  if (!fill_address (min_address, min_port, &range->min, is_multicast))
    goto invalid;
  if (!fill_address (max_address, max_port, &range->max, is_multicast))
    goto invalid;

  if (range->min.size != range->max.size)
    goto invalid;
  if (memcmp (range->min.bytes, range->max.bytes, range->min.size) > 0)
    goto invalid;

  range->ttl = ttl;

  GST_DEBUG_OBJECT (pool, "adding %s-%s:%u-%u ttl %u", min_address, max_address,
      min_port, max_port, ttl);

  g_mutex_lock (&priv->lock);
  priv->addresses = g_list_prepend (priv->addresses, range);

  if (!is_multicast)
    priv->has_unicast_addresses = TRUE;
  g_mutex_unlock (&priv->lock);

  return TRUE;

  /* ERRORS */
invalid:
  {
    GST_ERROR_OBJECT (pool, "invalid address range %s-%s", min_address,
        max_address);
    g_slice_free (AddrRange, range);
    return FALSE;
  }
}

static void
inc_address (Addr * addr, guint count)
{
  gint i;
  guint carry;

  carry = count;
  for (i = addr->size - 1; i >= 0 && carry > 0; i--) {
    carry += addr->bytes[i];
    addr->bytes[i] = carry & 0xff;
    carry >>= 8;
  }
}

/* tells us the number of addresses between min_addr and max_addr */
static guint
diff_address (Addr * max_addr, Addr * min_addr)
{
  gint i;
  guint result = 0;

  g_return_val_if_fail (min_addr->size == max_addr->size, 0);

  for (i = 0; i < min_addr->size; i++) {
    g_return_val_if_fail (result < (1 << 24), result);

    result <<= 8;
    result += max_addr->bytes[i] - min_addr->bytes[i];
  }

  return result;
}

static AddrRange *
split_range (GstRTSPAddressPool * pool, AddrRange * range, guint skip_addr,
    guint skip_port, gint n_ports)
{
  GstRTSPAddressPoolPrivate *priv = pool->priv;
  AddrRange *temp;

  if (skip_addr) {
    temp = g_slice_dup (AddrRange, range);
    memcpy (temp->max.bytes, temp->min.bytes, temp->min.size);
    inc_address (&temp->max, skip_addr - 1);
    priv->addresses = g_list_prepend (priv->addresses, temp);

    inc_address (&range->min, skip_addr);
  }

  if (!RANGE_IS_SINGLE (range)) {
    /* min and max are not the same, we have more than one address. */
    temp = g_slice_dup (AddrRange, range);
    /* increment the range min address */
    inc_address (&temp->min, 1);
    /* and store back in pool */
    priv->addresses = g_list_prepend (priv->addresses, temp);

    /* adjust range with only the first address */
    memcpy (range->max.bytes, range->min.bytes, range->min.size);
  }

  /* range now contains only one single address */
  if (skip_port > 0) {
    /* make a range with the skipped ports */
    temp = g_slice_dup (AddrRange, range);
    temp->max.port = temp->min.port + skip_port - 1;
    /* and store back in pool */
    priv->addresses = g_list_prepend (priv->addresses, temp);

    /* increment range port */
    range->min.port += skip_port;
  }
  /* range now contains single address with desired port number */
  if (range->max.port - range->min.port + 1 > n_ports) {
    /* make a range with the remaining ports */
    temp = g_slice_dup (AddrRange, range);
    temp->min.port += n_ports;
    /* and store back in pool */
    priv->addresses = g_list_prepend (priv->addresses, temp);

    /* and truncate port */
    range->max.port = range->min.port + n_ports - 1;
  }
  return range;
}

/**
 * gst_rtsp_address_pool_acquire_address:
 * @pool: a #GstRTSPAddressPool
 * @flags: flags
 * @n_ports: the amount of ports
 *
 * Take an address and ports from @pool. @flags can be used to control the
 * allocation. @n_ports consecutive ports will be allocated of which the first
 * one can be found in @port.
 *
 * Returns: a #GstRTSPAddress that should be freed with gst_rtsp_address_free
 *   after use or %NULL when no address could be acquired.
 */
GstRTSPAddress *
gst_rtsp_address_pool_acquire_address (GstRTSPAddressPool * pool,
    GstRTSPAddressFlags flags, gint n_ports)
{
  GstRTSPAddressPoolPrivate *priv;
  GList *walk, *next;
  AddrRange *result;
  GstRTSPAddress *addr;

  g_return_val_if_fail (GST_IS_RTSP_ADDRESS_POOL (pool), NULL);
  g_return_val_if_fail (n_ports > 0, NULL);

  priv = pool->priv;
  result = NULL;
  addr = NULL;

  g_mutex_lock (&priv->lock);
  /* go over available ranges */
  for (walk = priv->addresses; walk; walk = next) {
    AddrRange *range;
    gint ports, skip;

    range = walk->data;
    next = walk->next;

    /* check address type when given */
    if (flags & GST_RTSP_ADDRESS_FLAG_IPV4 && !ADDR_IS_IPV4 (&range->min))
      continue;
    if (flags & GST_RTSP_ADDRESS_FLAG_IPV6 && !ADDR_IS_IPV6 (&range->min))
      continue;
    if (flags & GST_RTSP_ADDRESS_FLAG_MULTICAST && range->ttl == 0)
      continue;
    if (flags & GST_RTSP_ADDRESS_FLAG_UNICAST && range->ttl != 0)
      continue;

    /* check for enough ports */
    ports = range->max.port - range->min.port + 1;
    if (flags & GST_RTSP_ADDRESS_FLAG_EVEN_PORT
        && !ADDR_IS_EVEN_PORT (&range->min))
      skip = 1;
    else
      skip = 0;
    if (ports - skip < n_ports)
      continue;

    /* we found a range, remove from the list */
    priv->addresses = g_list_delete_link (priv->addresses, walk);
    /* now split and exit our loop */
    result = split_range (pool, range, 0, skip, n_ports);
    priv->allocated = g_list_prepend (priv->allocated, result);
    break;
  }
  g_mutex_unlock (&priv->lock);

  if (result) {
    addr = g_slice_new0 (GstRTSPAddress);
    addr->pool = g_object_ref (pool);
    addr->address = get_address_string (&result->min);
    addr->n_ports = n_ports;
    addr->port = result->min.port;
    addr->ttl = result->ttl;
    addr->priv = result;

    GST_DEBUG_OBJECT (pool, "got address %s:%u ttl %u", addr->address,
        addr->port, addr->ttl);
  }

  return addr;
}

/**
 * gst_rtsp_address_pool_release_address:
 * @pool: a #GstRTSPAddressPool
 * @id: an address id
 *
 * Release a previously acquired address (with
 * gst_rtsp_address_pool_acquire_address()) back into @pool.
 */
static void
gst_rtsp_address_pool_release_address (GstRTSPAddressPool * pool,
    GstRTSPAddress * addr)
{
  GstRTSPAddressPoolPrivate *priv;
  GList *find;
  AddrRange *range;

  g_return_if_fail (GST_IS_RTSP_ADDRESS_POOL (pool));
  g_return_if_fail (addr != NULL);
  g_return_if_fail (addr->pool == pool);

  priv = pool->priv;
  range = addr->priv;

  /* we don't want to free twice */
  addr->priv = NULL;
  addr->pool = NULL;

  g_mutex_lock (&priv->lock);
  find = g_list_find (priv->allocated, range);
  if (find == NULL)
    goto not_found;

  priv->allocated = g_list_delete_link (priv->allocated, find);

  /* FIXME, merge and do something clever */
  priv->addresses = g_list_prepend (priv->addresses, range);
  g_mutex_unlock (&priv->lock);

  g_object_unref (pool);

  return;

  /* ERRORS */
not_found:
  {
    g_warning ("Released unknown address %p", addr);
    g_mutex_unlock (&priv->lock);
    return;
  }
}

static void
dump_range (AddrRange * range, GstRTSPAddressPool * pool)
{
  gchar *addr1, *addr2;

  addr1 = get_address_string (&range->min);
  addr2 = get_address_string (&range->max);
  g_print ("  address %s-%s, port %u-%u, ttl %u\n", addr1, addr2,
      range->min.port, range->max.port, range->ttl);
  g_free (addr1);
  g_free (addr2);
}

/**
 * gst_rtsp_address_pool_dump:
 * @pool: a #GstRTSPAddressPool
 *
 * Dump the free and allocated addresses to stdout.
 */
void
gst_rtsp_address_pool_dump (GstRTSPAddressPool * pool)
{
  GstRTSPAddressPoolPrivate *priv;

  g_return_if_fail (GST_IS_RTSP_ADDRESS_POOL (pool));

  priv = pool->priv;

  g_mutex_lock (&priv->lock);
  g_print ("free:\n");
  g_list_foreach (priv->addresses, (GFunc) dump_range, pool);
  g_print ("allocated:\n");
  g_list_foreach (priv->allocated, (GFunc) dump_range, pool);
  g_mutex_unlock (&priv->lock);
}

static GList *
find_address_in_ranges (GList * addresses, Addr * addr, guint port,
    guint n_ports, guint ttl)
{
  GList *walk, *next;

  /* go over available ranges */
  for (walk = addresses; walk; walk = next) {
    AddrRange *range;

    range = walk->data;
    next = walk->next;

    /* Not the right type of address */
    if (range->min.size != addr->size)
      continue;

    /* Check that the address is in the interval */
    if (memcmp (range->min.bytes, addr->bytes, addr->size) > 0 ||
        memcmp (range->max.bytes, addr->bytes, addr->size) < 0)
      continue;

    /* Make sure the requested ports are inside the range */
    if (port < range->min.port || port + n_ports - 1 > range->max.port)
      continue;

    if (ttl != range->ttl)
      continue;

    break;
  }

  return walk;
}

/**
 * gst_rtsp_address_pool_reserve_address:
 * @pool: a #GstRTSPAddressPool
 * @ip_address: The IP address to reserve
 * @port: The first port to reserve
 * @n_ports: The number of ports
 * @ttl: The requested ttl
 * @address: (out) storage for a #GstRTSPAddress
 *
 * Take a specific address and ports from @pool. @n_ports consecutive
 * ports will be allocated of which the first one can be found in
 * @port.
 *
 * If @ttl is 0, @address should be a unicast address. If @ttl > 0, @address
 * should be a valid multicast address.
 *
 * Returns: #GST_RTSP_ADDRESS_POOL_OK if an address was reserved. The address
 * is returned in @address and should be freed with gst_rtsp_address_free
 * after use.
 */
GstRTSPAddressPoolResult
gst_rtsp_address_pool_reserve_address (GstRTSPAddressPool * pool,
    const gchar * ip_address, guint port, guint n_ports, guint ttl,
    GstRTSPAddress ** address)
{
  GstRTSPAddressPoolPrivate *priv;
  Addr input_addr;
  GList *list;
  AddrRange *addr_range;
  GstRTSPAddress *addr;
  gboolean is_multicast;
  GstRTSPAddressPoolResult result;

  g_return_val_if_fail (GST_IS_RTSP_ADDRESS_POOL (pool),
      GST_RTSP_ADDRESS_POOL_EINVAL);
  g_return_val_if_fail (ip_address != NULL, GST_RTSP_ADDRESS_POOL_EINVAL);
  g_return_val_if_fail (port > 0, GST_RTSP_ADDRESS_POOL_EINVAL);
  g_return_val_if_fail (n_ports > 0, GST_RTSP_ADDRESS_POOL_EINVAL);
  g_return_val_if_fail (address != NULL, GST_RTSP_ADDRESS_POOL_EINVAL);

  priv = pool->priv;
  addr_range = NULL;
  addr = NULL;
  is_multicast = ttl != 0;

  if (!fill_address (ip_address, port, &input_addr, is_multicast))
    goto invalid;

  g_mutex_lock (&priv->lock);
  list = find_address_in_ranges (priv->addresses, &input_addr, port, n_ports,
      ttl);
  if (list != NULL) {
    AddrRange *range = list->data;
    guint skip_port, skip_addr;

    skip_addr = diff_address (&input_addr, &range->min);
    skip_port = port - range->min.port;

    GST_DEBUG_OBJECT (pool, "diff 0x%08x/%u", skip_addr, skip_port);

    /* we found a range, remove from the list */
    priv->addresses = g_list_delete_link (priv->addresses, list);
    /* now split and exit our loop */
    addr_range = split_range (pool, range, skip_addr, skip_port, n_ports);
    priv->allocated = g_list_prepend (priv->allocated, addr_range);
  }

  if (addr_range) {
    addr = g_slice_new0 (GstRTSPAddress);
    addr->pool = g_object_ref (pool);
    addr->address = get_address_string (&addr_range->min);
    addr->n_ports = n_ports;
    addr->port = addr_range->min.port;
    addr->ttl = addr_range->ttl;
    addr->priv = addr_range;

    result = GST_RTSP_ADDRESS_POOL_OK;
    GST_DEBUG_OBJECT (pool, "reserved address %s:%u ttl %u", addr->address,
        addr->port, addr->ttl);
  } else {
    /* We failed to reserve the address. Check if it was because the address
     * was already in use or if it wasn't in the pool to begin with */
    list = find_address_in_ranges (priv->allocated, &input_addr, port, n_ports,
        ttl);
    if (list != NULL) {
      result = GST_RTSP_ADDRESS_POOL_ERESERVED;
    } else {
      result = GST_RTSP_ADDRESS_POOL_ERANGE;
    }
  }
  g_mutex_unlock (&priv->lock);

  *address = addr;
  return result;

  /* ERRORS */
invalid:
  {
    GST_ERROR_OBJECT (pool, "invalid address %s:%u/%u/%u", ip_address,
        port, n_ports, ttl);
    *address = NULL;
    return GST_RTSP_ADDRESS_POOL_EINVAL;
  }
}

/**
 * gst_rtsp_address_pool_has_unicast_addresses:
 * @pool: a #GstRTSPAddressPool
 *
 * Used to know if the pool includes any unicast addresses.
 *
 * Returns: %TRUE if the pool includes any unicast addresses, %FALSE otherwise
 */

gboolean
gst_rtsp_address_pool_has_unicast_addresses (GstRTSPAddressPool * pool)
{
  GstRTSPAddressPoolPrivate *priv;
  gboolean has_unicast_addresses;

  g_return_val_if_fail (GST_IS_RTSP_ADDRESS_POOL (pool), FALSE);

  priv = pool->priv;

  g_mutex_lock (&priv->lock);
  has_unicast_addresses = priv->has_unicast_addresses;
  g_mutex_unlock (&priv->lock);

  return has_unicast_addresses;
}
