/*
   Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.

   This program 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; version 2 of the License.

   This program 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 this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
*/

#include <NdbMutex.h>
#include <NdbThread.h>
#ifdef NDB_MUTEX_DEADLOCK_DETECTOR

#include "NdbMutex_DeadlockDetector.h"

#define NDB_THREAD_TLS_SELF NDB_THREAD_TLS_MAX

static NdbMutex g_mutex_no_mutex; // We need a mutex to assign numbers to mutexes...
static nmdd_mask g_mutex_no_mask = { 0, 0 };

static unsigned alloc_mutex_no();
static void release_mutex_no(unsigned no);

static NdbMutex* get_element(struct nmdd_mutex_array *, unsigned i);
static void add_mutex_to_array(struct nmdd_mutex_array *, NdbMutex*);
static void remove_mutex_from_array(struct nmdd_mutex_array *, NdbMutex*);

static void set_bit(struct nmdd_mask *, unsigned no);
static void clear_bit(struct nmdd_mask* , unsigned no);
static bool check_bit(const struct nmdd_mask* , unsigned no);

static void release(struct nmdd_mutex_array *);
static void release(struct nmdd_mask*);

extern "C"
void
NdbMutex_DeadlockDetectorInit()
{
  NdbMutex_Init(&g_mutex_no_mutex);
}

extern "C"
void
NdbMutex_DeadlockDetectorEnd()
{
  release(&g_mutex_no_mask);
}

extern "C"
void
ndb_mutex_created(NdbMutex* p)
{
  p->m_mutex_state = (ndb_mutex_state*)malloc(sizeof(ndb_mutex_state));
  bzero(p->m_mutex_state, sizeof(ndb_mutex_state));

  /**
   * Assign mutex no
   */
  p->m_mutex_state->m_no = alloc_mutex_no();
}

extern "C"
void
ndb_mutex_destoyed(NdbMutex* p)
{
  unsigned no = p->m_mutex_state->m_no;

  /**
   * In order to be able to reuse mutex_no,
   *   we need to clear this no from all mutexes that has it in before map...
   *   this is all mutexes in after map
   */
  for (unsigned i = 0; i<p->m_mutex_state->m_locked_after_list.m_used; i++)
  {
    NdbMutex * m = get_element(&p->m_mutex_state->m_locked_after_list, i);
    assert(check_bit(&p->m_mutex_state->m_locked_after_mask, m->m_mutex_state->m_no));

    /**
     * And we need to lock it while doing this
     */
    NdbMutex_Lock(m);
    assert(check_bit(&m->m_mutex_state->m_locked_before_mask, no));
    clear_bit(&m->m_mutex_state->m_locked_before_mask, no);
    remove_mutex_from_array(&m->m_mutex_state->m_locked_before_list, p);
    NdbMutex_Unlock(m);
  }

  /**
   * And we need to remove ourselfs from after list of mutexes in out before list
   */
  for (unsigned i = 0; i<p->m_mutex_state->m_locked_before_list.m_used; i++)
  {
    NdbMutex * m = get_element(&p->m_mutex_state->m_locked_before_list, i);
    NdbMutex_Lock(m);
    assert(check_bit(&m->m_mutex_state->m_locked_after_mask, no));
    clear_bit(&m->m_mutex_state->m_locked_after_mask, no);
    remove_mutex_from_array(&m->m_mutex_state->m_locked_after_list, p);
    NdbMutex_Unlock(m);
  }

  release(&p->m_mutex_state->m_locked_before_mask);
  release(&p->m_mutex_state->m_locked_before_list);
  release(&p->m_mutex_state->m_locked_after_mask);
  release(&p->m_mutex_state->m_locked_after_list);
  release_mutex_no(no);
}

static
ndb_mutex_thr_state*
get_thr()
{
  void * p = NdbThread_GetTlsKey(NDB_THREAD_TLS_SELF);
  return (ndb_mutex_thr_state*)p;
}

#define INC_SIZE 16

static
void
add_lock_to_thread(ndb_mutex_thr_state * t, NdbMutex * m)
{
  add_mutex_to_array(&t->m_mutexes_locked, m);
}

static
void
add_lock_to_mutex_before_list(ndb_mutex_state * m1, NdbMutex * m2)
{
  assert(m1 != m2->m_mutex_state);
  unsigned no = m2->m_mutex_state->m_no;
  if (!check_bit(&m1->m_locked_before_mask, no))
  {
    set_bit(&m1->m_locked_before_mask, no);
    add_mutex_to_array(&m1->m_locked_before_list, m2);
  }
}

static
void
add_lock_to_mutex_after_list(ndb_mutex_state * m1, NdbMutex* m2)
{
  assert(m1 != m2->m_mutex_state);
  unsigned no = m2->m_mutex_state->m_no;
  if (!check_bit(&m1->m_locked_after_mask, no))
  {
    set_bit(&m1->m_locked_after_mask, no);
    add_mutex_to_array(&m1->m_locked_after_list, m2);
  }
}

extern "C"
void
ndb_mutex_locked(NdbMutex* p)
{
  ndb_mutex_state * m = p->m_mutex_state;
  ndb_mutex_thr_state * thr = get_thr();
  if (thr == 0)
  {
    /**
     * These are threads not started with NdbThread_Create(...)
     *   e.g mysql-server threads...ignore these for now
     */
    return;
  }

  for (unsigned i = 0; i < thr->m_mutexes_locked.m_used; i++)
  {
    /**
     * We want to lock m
     * Check that none of the mutex we curreny have locked
     *   have m in their *before* list
     */
    NdbMutex * h = get_element(&thr->m_mutexes_locked, i);
    if (check_bit(&h->m_mutex_state->m_locked_before_mask, m->m_no))
    {
      abort();
    }

    /**
     * Add h to m's list of before-locks
     */
    add_lock_to_mutex_before_list(m, h);

    /**
     * Add m to h's list of after locks
     */
    add_lock_to_mutex_after_list(h->m_mutex_state, p);
  }

  add_lock_to_thread(thr, p);
}

extern "C"
void
ndb_mutex_unlocked(NdbMutex* m)
{
  ndb_mutex_thr_state * thr = get_thr();
  if (thr == 0)
  {
    /**
     * These are threads not started with NdbThread_Create(...)
     *   e.g mysql-server threads...ignore these for now
     */
    return;
  }
  unsigned pos = thr->m_mutexes_locked.m_used;
  assert(pos > 0);
  assert(get_element(&thr->m_mutexes_locked, pos-1) == m);
  thr->m_mutexes_locked.m_used --;
}

extern "C"
void
ndb_mutex_try_locked(NdbMutex* p)
{

}

extern "C"
void
ndb_mutex_thread_init(struct ndb_mutex_thr_state* p)
{
  bzero(p, sizeof(* p));
  NdbThread_SetTlsKey(NDB_THREAD_TLS_SELF, p);
}

extern "C"
void
ndb_mutex_thread_exit()
{
  ndb_mutex_thr_state * thr = get_thr();
  if (thr == 0)
  {
    /**
     * These are threads not started with NdbThread_Create(...)
     *   e.g mysql-server threads...ignore these for now
     */
    return;
  }
  release(&thr->m_mutexes_locked);
}

/**
 * util
 */
static
void
set_bit(nmdd_mask * mask, unsigned no)
{
  unsigned byte_no = no / 8;
  unsigned bit_no = no & 7;
  if (byte_no >= mask->m_len)
  {
    unsigned new_len = mask->m_len + INC_SIZE;
    if (byte_no >= new_len)
    {
      new_len = byte_no + 1;
    }
    unsigned char * new_arr = (unsigned char*)malloc(new_len);
    bzero(new_arr, new_len);
    if (mask->m_len != 0)
    {
      memcpy(new_arr, mask->m_mask, mask->m_len);
      free(mask->m_mask);
    }
    mask->m_len = new_len;
    mask->m_mask = new_arr;
  }

  mask->m_mask[byte_no] |= (1 << bit_no);
}

static
void
clear_bit(nmdd_mask * mask, unsigned no)
{
  unsigned byte_no = no / 8;
  unsigned bit_no = no & 7;
  if (byte_no >= mask->m_len)
  {
    return;
  }

  mask->m_mask[byte_no] &= ~(unsigned char)(1 << bit_no);
}

static
bool
check_bit(const nmdd_mask * mask, unsigned no)
{
  unsigned byte_no = no / 8;
  unsigned bit_no = no & 7;
  if (byte_no >= mask->m_len)
  {
    return false;
  }

  return (mask->m_mask[byte_no] & (1 << bit_no)) != 0;
}

static
void
release(nmdd_mask * mask)
{
  if (mask->m_len != 0)
  {
    free(mask->m_mask);
  }
}

static
NdbMutex*
get_element(nmdd_mutex_array* arr, unsigned i)
{
  assert(i < arr->m_used);
  return arr->m_array[i];
}

static
void
add_mutex_to_array(nmdd_mutex_array* arr, NdbMutex* m)
{
  unsigned pos = arr->m_used;
  if (arr->m_used == arr->m_array_len)
  {
    unsigned new_len = arr->m_array_len + INC_SIZE;
    NdbMutex** new_arr = (NdbMutex**)malloc(new_len * sizeof(NdbMutex*));
    if (arr->m_array_len != 0)
    {
      memcpy(new_arr, arr->m_array, arr->m_array_len * sizeof(NdbMutex*));
      free(arr->m_array);
    }
    arr->m_array = new_arr;
    arr->m_array_len = new_len;
  }
  for (unsigned i = 0; i<arr->m_used; i++)
    assert(arr->m_array[i] != m);

  arr->m_array[pos] = m;
  arr->m_used++;
}

static
void
remove_mutex_from_array(nmdd_mutex_array* arr, NdbMutex* m)
{
  for (unsigned i = 0; i < arr->m_used; i++)
  {
    unsigned idx = arr->m_used - i - 1;
    if (arr->m_array[idx] == m)
    {
      memmove(arr->m_array+idx,
              arr->m_array + idx + 1,
              i * sizeof(NdbMutex*));
      arr->m_used--;
      return;
    }
  }
  assert(false);
}

static
void
release(nmdd_mutex_array* arr)
{
  if (arr->m_array_len)
  {
    free(arr->m_array);
  }
}

static
unsigned
ff(unsigned char b)
{
  for (unsigned i = 0; i<8; i++)
    if ((b & (1 << i)) == 0)
      return i;
  assert(false);
}

static
unsigned
alloc_mutex_no()
{
  Guard g(&g_mutex_no_mutex);
  unsigned no = 0;

  for (unsigned i = 0; i < g_mutex_no_mask.m_len; i++)
  {
    if (g_mutex_no_mask.m_mask[i] != 255)
    {
      no = (8 * i) + ff(g_mutex_no_mask.m_mask[i]);
      goto found;
    }
  }

  no = 8 * g_mutex_no_mask.m_len;
found:
  set_bit(&g_mutex_no_mask, no);
  assert(check_bit(&g_mutex_no_mask, no));
  return no;
}

static
void
release_mutex_no(unsigned no)
{
  Guard g(&g_mutex_no_mutex);
  assert(check_bit(&g_mutex_no_mask, no));
  clear_bit(&g_mutex_no_mask, no);
}

#endif
