/*
 * Copyright (C) 2011, 2012, 2013 Citrix Systems
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the project nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include "ns_turn_allocation.h"
#include "ns_turn_ioalib.h"

/////////////// Permission forward declarations /////////////////

static void init_turn_permission_hashtable(turn_permission_hashtable *map);
static void free_turn_permission_hashtable(turn_permission_hashtable *map);
static turn_permission_info *get_from_turn_permission_hashtable(turn_permission_hashtable *map, const ioa_addr *addr);

/////////////// ALLOCATION //////////////////////////////////////

void init_allocation(void *owner, allocation *a, ur_map *tcp_connections) {
  if (a) {
    memset(a, 0, sizeof(allocation));
    a->owner = owner;
    a->tcp_connections = tcp_connections;
    init_turn_permission_hashtable(&(a->addr_to_perm));
  }
}

void clear_allocation(allocation *a, SOCKET_TYPE socket_type) {
  if (a) {

    if (a->is_valid)
      turn_report_allocation_delete(a, socket_type);

    if (a->tcs.elems) {
      size_t i;
      size_t sz = a->tcs.sz;
      for (i = 0; i < sz; ++i) {
        tcp_connection *tc = a->tcs.elems[i];
        if (tc) {
          delete_tcp_connection(tc);
          a->tcs.elems[i] = NULL;
        }
      }
      free(a->tcs.elems);
      a->tcs.elems = NULL;
    }
    a->tcs.sz = 0;

    {
      int i;
      for (i = 0; i < ALLOC_PROTOCOLS_NUMBER; ++i) {
        clear_ioa_socket_session_if(a->relay_sessions[i].s, a->owner);
        clear_relay_endpoint_session_data(&(a->relay_sessions[i]));
        IOA_EVENT_DEL(a->relay_sessions[i].lifetime_ev);
      }
    }

    /* The order is important here: */
    free_turn_permission_hashtable(&(a->addr_to_perm));
    ch_map_clean(&(a->chns));

    a->is_valid = 0;
  }
}

relay_endpoint_session *get_relay_session(allocation *a, int family) {
  if (a)
    return &(a->relay_sessions[ALLOC_INDEX(family)]);
  return NULL;
}

int get_relay_session_failure(allocation *a, int family) {
  if (a)
    return a->relay_sessions_failure[ALLOC_INDEX(family)];
  return 0;
}

void set_relay_session_failure(allocation *a, int family) {
  if (a)
    a->relay_sessions_failure[ALLOC_INDEX(family)] = 1;
}

ioa_socket_handle get_relay_socket(allocation *a, int family) {
  if (a)
    return a->relay_sessions[ALLOC_INDEX(family)].s;
  return NULL;
}

void set_allocation_family_invalid(allocation *a, int family) {
  if (a) {
    size_t index = ALLOC_INDEX(family);
    if (a->relay_sessions[index].s) {
      if (a->tcs.elems) {
        size_t i;
        size_t sz = a->tcs.sz;
        for (i = 0; i < sz; ++i) {
          tcp_connection *tc = a->tcs.elems[i];
          if (tc) {
            if (tc->peer_s && (get_ioa_socket_address_family(tc->peer_s) == family)) {
              delete_tcp_connection(tc);
              a->tcs.elems[i] = NULL;
            }
          }
        }
      }

      clear_ioa_socket_session_if(a->relay_sessions[index].s, a->owner);
      clear_relay_endpoint_session_data(&(a->relay_sessions[index]));
      IOA_EVENT_DEL(a->relay_sessions[index].lifetime_ev);
    }
  }
}

void set_allocation_lifetime_ev(allocation *a, turn_time_t exp_time, ioa_timer_handle ev, int family) {
  if (a) {
    IOA_EVENT_DEL(a->relay_sessions[ALLOC_INDEX(family)].lifetime_ev);
    a->relay_sessions[ALLOC_INDEX(family)].expiration_time = exp_time;
    a->relay_sessions[ALLOC_INDEX(family)].lifetime_ev = ev;
  }
}

int is_allocation_valid(const allocation *a) {
  if (a)
    return a->is_valid;
  else
    return 0;
}

void set_allocation_valid(allocation *a, int value) {
  if (a)
    a->is_valid = value;
}

turn_permission_info *allocation_get_permission(allocation *a, const ioa_addr *addr) {
  if (a) {
    return get_from_turn_permission_hashtable(&(a->addr_to_perm), addr);
  }
  return NULL;
}

///////////////////////////// TURN_PERMISSION /////////////////////////////////

static int delete_channel_info_from_allocation_map(ur_map_key_type key, ur_map_value_type value);

void turn_permission_clean(turn_permission_info *tinfo) {
  if (tinfo && tinfo->allocated) {

    if (tinfo->verbose) {
      char s[257] = "\0";
      addr_to_string(&(tinfo->addr), (uint8_t *)s);
      TURN_LOG_FUNC(TURN_LOG_LEVEL_INFO, "session %018llu: peer %s deleted\n", tinfo->session_id, s);
    }

    if (!(tinfo->lifetime_ev)) {
      TURN_LOG_FUNC(TURN_LOG_LEVEL_ERROR, "!!! %s: strange (1) permission to be cleaned\n", __FUNCTION__);
    }

    IOA_EVENT_DEL(tinfo->lifetime_ev);
    lm_map_foreach(&(tinfo->chns), (foreachcb_type)delete_channel_info_from_allocation_map);
    lm_map_clean(&(tinfo->chns));
    memset(tinfo, 0, sizeof(turn_permission_info));
  }
}

static void init_turn_permission_hashtable(turn_permission_hashtable *map) {
  if (map)
    memset(map, 0, sizeof(turn_permission_hashtable));
}

static void free_turn_permission_hashtable(turn_permission_hashtable *map) {
  if (map) {

    size_t i;
    for (i = 0; i < TURN_PERMISSION_HASHTABLE_SIZE; ++i) {

      turn_permission_array *parray = &(map->table[i]);

      {
        size_t j;
        for (j = 0; j < TURN_PERMISSION_ARRAY_SIZE; ++j) {
          turn_permission_slot *slot = &(parray->main_slots[j]);
          if (slot->info.allocated) {
            turn_permission_clean(&(slot->info));
          }
        }
      }

      if (parray->extra_slots) {
        size_t j;
        for (j = 0; j < parray->extra_sz; ++j) {
          turn_permission_slot *slot = parray->extra_slots[j];
          if (slot) {
            if (slot->info.allocated) {
              turn_permission_clean(&(slot->info));
            }
            free(slot);
          }
        }
        free(parray->extra_slots);
        parray->extra_slots = NULL;
      }
      parray->extra_sz = 0;
    }
  }
}

static turn_permission_info *get_from_turn_permission_hashtable(turn_permission_hashtable *map, const ioa_addr *addr) {
  if (!addr || !map)
    return NULL;

  uint32_t index = addr_hash_no_port(addr) & (TURN_PERMISSION_HASHTABLE_SIZE - 1);
  turn_permission_array *parray = &(map->table[index]);

  {
    size_t i;
    for (i = 0; i < TURN_PERMISSION_ARRAY_SIZE; ++i) {
      turn_permission_slot *slot = &(parray->main_slots[i]);
      if (slot->info.allocated && addr_eq_no_port(&(slot->info.addr), addr)) {
        return &(slot->info);
      }
    }
  }

  if (parray->extra_slots) {

    size_t i;
    size_t sz = parray->extra_sz;
    for (i = 0; i < sz; ++i) {
      turn_permission_slot *slot = parray->extra_slots[i];
      if (slot->info.allocated && addr_eq_no_port(&(slot->info.addr), addr)) {
        return &(slot->info);
      }
    }
  }

  return NULL;
}

static void ch_info_clean(ch_info *c) {
  if (c) {
    if (c->kernel_channel) {
      DELETE_TURN_CHANNEL_KERNEL(c->kernel_channel);
      c->kernel_channel = 0;
    }
    IOA_EVENT_DEL(c->lifetime_ev);
    memset(c, 0, sizeof(ch_info));
  }
}

static int delete_channel_info_from_allocation_map(ur_map_key_type key, ur_map_value_type value) {
  UNUSED_ARG(key);

  if (value) {
    ch_info *chn = (ch_info *)value;

    if (chn->chnum < 1) {
      TURN_LOG_FUNC(TURN_LOG_LEVEL_ERROR, "!!! %s: strange (0) channel to be cleaned: chnum<1\n", __FUNCTION__);
    }

    ch_info_clean(chn);
  }

  return 0;
}

void turn_channel_delete(ch_info *chn) {
  if (chn) {
    int port = addr_get_port(&(chn->peer_addr));
    if (port < 1) {
      char s[129];
      addr_to_string(&(chn->peer_addr), (uint8_t *)s);
      TURN_LOG_FUNC(TURN_LOG_LEVEL_ERROR, "!!! %s: strange (1) channel to be cleaned: port is empty: %s\n",
                    __FUNCTION__, s);
    }
    {
      turn_permission_info *tinfo = (turn_permission_info *)chn->owner;
      if (tinfo) {
        lm_map_del(&(tinfo->chns), (ur_map_key_type)port, NULL);
      } else {
        TURN_LOG_FUNC(TURN_LOG_LEVEL_ERROR, "!!! %s: strange (2) channel to be cleaned: permission is empty\n",
                      __FUNCTION__);
      }
    }
    delete_channel_info_from_allocation_map((ur_map_key_type)port, (ur_map_value_type)chn);
  }
}

ch_info *allocation_get_new_ch_info(allocation *a, uint16_t chnum, ioa_addr *peer_addr) {

  turn_permission_info *tinfo = get_from_turn_permission_hashtable(&(a->addr_to_perm), peer_addr);

  if (!tinfo)
    tinfo = allocation_add_permission(a, peer_addr);

  ch_info *chn = ch_map_get(&(a->chns), chnum, 1);

  chn->allocated = 1;
  chn->chnum = chnum;
  chn->port = addr_get_port(peer_addr);
  addr_cpy(&(chn->peer_addr), peer_addr);
  chn->owner = tinfo;

  lm_map_put(&(tinfo->chns), (ur_map_key_type)addr_get_port(peer_addr), (ur_map_value_type)chn);

  return chn;
}

ch_info *allocation_get_ch_info(allocation *a, uint16_t chnum) { return ch_map_get(&(a->chns), chnum, 0); }

ch_info *allocation_get_ch_info_by_peer_addr(allocation *a, ioa_addr *peer_addr) {
  turn_permission_info *tinfo = get_from_turn_permission_hashtable(&(a->addr_to_perm), peer_addr);
  if (tinfo) {
    return get_turn_channel(tinfo, peer_addr);
  }
  return NULL;
}

uint16_t get_turn_channel_number(turn_permission_info *tinfo, ioa_addr *addr) {
  if (tinfo) {
    ur_map_value_type t = 0;
    if (lm_map_get(&(tinfo->chns), (ur_map_key_type)addr_get_port(addr), &t) && t) {
      ch_info *chn = (ch_info *)t;
      if (STUN_VALID_CHANNEL(chn->chnum)) {
        return chn->chnum;
      }
    }
  }

  return 0;
}

ch_info *get_turn_channel(turn_permission_info *tinfo, ioa_addr *addr) {
  if (tinfo) {
    ur_map_value_type t = 0;
    if (lm_map_get(&(tinfo->chns), (ur_map_key_type)addr_get_port(addr), &t) && t) {
      ch_info *chn = (ch_info *)t;
      if (STUN_VALID_CHANNEL(chn->chnum)) {
        return chn;
      }
    }
  }

  return NULL;
}

turn_permission_hashtable *allocation_get_turn_permission_hashtable(allocation *a) { return &(a->addr_to_perm); }

turn_permission_info *allocation_add_permission(allocation *a, const ioa_addr *addr) {
  if (a && addr) {

    turn_permission_hashtable *map = &(a->addr_to_perm);
    uint32_t hash = addr_hash_no_port(addr);
    size_t fds = (size_t)(hash & (TURN_PERMISSION_HASHTABLE_SIZE - 1));

    turn_permission_array *parray = &(map->table[fds]);

    turn_permission_slot *slot = NULL;

    {
      size_t i;
      for (i = 0; i < TURN_PERMISSION_ARRAY_SIZE; ++i) {
        slot = &(parray->main_slots[i]);
        if (!(slot->info.allocated)) {
          break;
        } else {
          slot = NULL;
        }
      }
    }

    if (!slot) {

      size_t old_sz = parray->extra_sz;

      turn_permission_slot **slots = parray->extra_slots;

      if (slots) {
        size_t i;
        for (i = 0; i < old_sz; ++i) {
          slot = slots[i];
          if (!(slot->info.allocated)) {
            break;
          } else {
            slot = NULL;
          }
        }
      }

      if (!slot) {
        size_t old_sz_mem = old_sz * sizeof(turn_permission_slot *);
        parray->extra_slots =
            (turn_permission_slot **)realloc(parray->extra_slots, old_sz_mem + sizeof(turn_permission_slot *));
        slots = parray->extra_slots;
        parray->extra_sz = old_sz + 1;
        slots[old_sz] = (turn_permission_slot *)malloc(sizeof(turn_permission_slot));
        slot = slots[old_sz];
      }
    }

    memset(slot, 0, sizeof(turn_permission_slot));
    slot->info.allocated = 1;
    turn_permission_info *elem = &(slot->info);
    addr_cpy(&(elem->addr), addr);
    elem->owner = a;

    return elem;
  } else {
    return NULL;
  }
}

ch_info *ch_map_get(ch_map *map, uint16_t chnum, int new_chn) {
  ch_info *ret = NULL;
  if (map) {
    size_t index = (size_t)(chnum & (CH_MAP_HASH_SIZE - 1));
    ch_map_array *a = &(map->table[index]);

    size_t i;
    for (i = 0; i < CH_MAP_ARRAY_SIZE; ++i) {
      ch_info *chi = &(a->main_chns[i]);
      if (chi->allocated) {
        if (!new_chn && (chi->chnum == chnum)) {
          return chi;
        }
      } else if (new_chn) {
        return chi;
      }
    }

    size_t old_sz = a->extra_sz;
    if (old_sz && a->extra_chns) {
      for (i = 0; i < old_sz; ++i) {
        ch_info *chi = a->extra_chns[i];
        if (chi) {
          if (chi->allocated) {
            if (!new_chn && (chi->chnum == chnum)) {
              return chi;
            }
          } else if (new_chn) {
            return chi;
          }
        }
      }
    }

    if (new_chn) {
      size_t old_sz_mem = old_sz * sizeof(ch_info *);
      a->extra_chns = (ch_info **)realloc(a->extra_chns, old_sz_mem + sizeof(ch_info *));
      a->extra_chns[old_sz] = (ch_info *)calloc(sizeof(ch_info), 1);
      a->extra_sz += 1;

      return a->extra_chns[old_sz];
    }
  }

  return ret;
}

void ch_map_clean(ch_map *map) {
  if (map) {
    size_t index;
    for (index = 0; index < CH_MAP_HASH_SIZE; ++index) {

      ch_map_array *a = &(map->table[index]);

      size_t i;
      for (i = 0; i < CH_MAP_ARRAY_SIZE; ++i) {
        ch_info *chi = &(a->main_chns[i]);
        if (chi->allocated) {
          ch_info_clean(chi);
        }
      }

      if (a->extra_chns) {
        size_t sz = a->extra_sz;
        for (i = 0; i < sz; ++i) {
          ch_info *chi = a->extra_chns[i];
          if (chi) {
            if (chi->allocated) {
              ch_info_clean(chi);
            }
            free(chi);
            a->extra_chns[i] = NULL;
          }
        }
        free(a->extra_chns);
        a->extra_chns = NULL;
      }
      a->extra_sz = 0;
    }
  }
}

////////////////// TCP connections ///////////////////////////////

static void set_new_tc_id(uint8_t server_id, tcp_connection *tc) {
  allocation *a = (allocation *)(tc->owner);
  ur_map *map = a->tcp_connections;
  uint32_t newid;
  uint32_t sid = server_id;
  sid = sid << 24;
  do {
    newid = 0;
    while (!newid) {
      newid = (uint32_t)turn_random();
      if (!newid) {
        continue;
      }
      newid = newid & 0x00FFFFFF;
      if (!newid) {
        continue;
      }
      newid = newid | sid;
    }
  } while (ur_map_get(map, (ur_map_key_type)newid, NULL));
  tc->id = newid;
  ur_map_put(map, (ur_map_key_type)newid, (ur_map_value_type)tc);
}

tcp_connection *create_tcp_connection(uint8_t server_id, allocation *a, stun_tid *tid, ioa_addr *peer_addr,
                                      int *err_code) {
  tcp_connection_list *tcl = &(a->tcs);
  if (tcl->elems) {
    size_t i;
    for (i = 0; i < tcl->sz; ++i) {
      tcp_connection *otc = tcl->elems[i];
      if (otc) {
        if (addr_eq(&(otc->peer_addr), peer_addr)) {
          *err_code = 446;
          return NULL;
        }
      }
    }
  }
  tcp_connection *tc = (tcp_connection *)calloc(sizeof(tcp_connection), 1);
  addr_cpy(&(tc->peer_addr), peer_addr);
  if (tid)
    memcpy(&(tc->tid), tid, sizeof(stun_tid));
  tc->owner = a;

  int found = 0;
  if (a->tcs.elems) {
    size_t i;
    for (i = 0; i < tcl->sz; ++i) {
      tcp_connection *otc = tcl->elems[i];
      if (!otc) {
        tcl->elems[i] = tc;
        found = 1;
        break;
      }
    }
  }

  if (!found) {
    size_t old_sz_mem = a->tcs.sz * sizeof(tcp_connection *);
    a->tcs.elems = (tcp_connection **)realloc(a->tcs.elems, old_sz_mem + sizeof(tcp_connection *));
    a->tcs.elems[a->tcs.sz] = tc;
    a->tcs.sz += 1;
    tcl = &(a->tcs);
  }

  set_new_tc_id(server_id, tc);
  return tc;
}

void delete_tcp_connection(tcp_connection *tc) {
  if (tc) {
    if (tc->done) {
      TURN_LOG_FUNC(TURN_LOG_LEVEL_INFO, "!!! %s: check on already closed tcp data connection: 0x%lx\n", __FUNCTION__,
                    (unsigned long)tc);
      return;
    }
    tc->done = 1;

    clear_unsent_buffer(&(tc->ub_to_client));

    IOA_EVENT_DEL(tc->peer_conn_timeout);
    IOA_EVENT_DEL(tc->conn_bind_timeout);
    allocation *a = (allocation *)(tc->owner);
    if (a) {
      ur_map *map = a->tcp_connections;
      if (map) {
        ur_map_del(map, (ur_map_key_type)(tc->id), NULL);
      }
      tcp_connection_list *tcl = &(a->tcs);
      if (tcl->elems) {
        size_t i;
        for (i = 0; i < tcl->sz; ++i) {
          if (tcl->elems[i] == tc) {
            tcl->elems[i] = NULL;
            break;
          }
        }
      }
    }
    IOA_CLOSE_SOCKET(tc->client_s);
    IOA_CLOSE_SOCKET(tc->peer_s);
    free(tc);
  }
}

tcp_connection *get_and_clean_tcp_connection_by_id(ur_map *map, tcp_connection_id id) {
  if (map) {
    ur_map_value_type t = 0;
    if (ur_map_get(map, (ur_map_key_type)id, &t) && t) {
      ur_map_del(map, (ur_map_key_type)id, NULL);
      return (tcp_connection *)t;
    }
  }
  return NULL;
}

tcp_connection *get_tcp_connection_by_id(ur_map *map, tcp_connection_id id) {
  if (map) {
    ur_map_value_type t = 0;
    if (ur_map_get(map, (ur_map_key_type)id, &t) && t) {
      return (tcp_connection *)t;
    }
  }
  return NULL;
}

tcp_connection *get_tcp_connection_by_peer(allocation *a, ioa_addr *peer_addr) {
  if (a && peer_addr) {
    tcp_connection_list *tcl = &(a->tcs);
    if (tcl->elems) {
      size_t i;
      size_t sz = tcl->sz;
      for (i = 0; i < sz; ++i) {
        tcp_connection *tc = tcl->elems[i];
        if (tc) {
          if (addr_eq(&(tc->peer_addr), peer_addr)) {
            return tc;
          }
        }
      }
    }
  }
  return NULL;
}

int can_accept_tcp_connection_from_peer(allocation *a, ioa_addr *peer_addr, int server_relay) {
  if (server_relay)
    return 1;

  if (a && peer_addr) {
    return (get_from_turn_permission_hashtable(&(a->addr_to_perm), peer_addr) != NULL);
  }

  return 0;
}

//////////////// Unsent buffers //////////////////////

void clear_unsent_buffer(unsent_buffer *ub) {
  if (ub) {
    if (ub->bufs) {
      size_t sz;
      for (sz = 0; sz < ub->sz; sz++) {
        ioa_network_buffer_handle nbh = ub->bufs[sz];
        if (nbh) {
          ioa_network_buffer_delete(NULL, nbh);
          ub->bufs[sz] = NULL;
        }
      }
      free(ub->bufs);
      ub->bufs = NULL;
    }
    ub->sz = 0;
  }
}

void add_unsent_buffer(unsent_buffer *ub, ioa_network_buffer_handle nbh) {
  if (!ub || (ub->sz >= MAX_UNSENT_BUFFER_SIZE)) {
    ioa_network_buffer_delete(NULL, nbh);
  } else {
    ub->bufs = (ioa_network_buffer_handle *)realloc(ub->bufs, sizeof(ioa_network_buffer_handle) * (ub->sz + 1));
    ub->bufs[ub->sz] = nbh;
    ub->sz += 1;
  }
}

ioa_network_buffer_handle top_unsent_buffer(unsent_buffer *ub) {
  ioa_network_buffer_handle ret = NULL;
  if (ub && ub->bufs && ub->sz) {
    size_t sz;
    for (sz = 0; sz < ub->sz; ++sz) {
      if (ub->bufs[sz]) {
        ret = ub->bufs[sz];
        break;
      }
    }
  }
  return ret;
}

void pop_unsent_buffer(unsent_buffer *ub) {
  if (ub && ub->bufs && ub->sz) {
    size_t sz;
    for (sz = 0; sz < ub->sz; ++sz) {
      if (ub->bufs[sz]) {
        ub->bufs[sz] = NULL;
        break;
      }
    }
  }
}

//////////////////////////////////////////////////////////////////
