/* Dispatch signal to right virtual memory area.
   Copyright (C) 1993-1999, 2002-2003, 2016  Bruno Haible <bruno@clisp.org>

   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; either version 2, or (at your option)
   any later version.

   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 Street, Fifth Floor, Boston, MA 02110-1301, USA.  */

#include "sigsegv.h"

#include <stdint.h>
#include <stdlib.h>
#ifdef _WIN32
#include <malloc.h>
#endif

/*
 * A dispatcher contains an AVL tree of non-empty intervals, sorted according
 * to their starting address.
 */
typedef
struct node_t
{
  /* AVL tree management.  */
  struct node_t *left;
  struct node_t *right;
  unsigned int height;
  /* Representation of interval.  */
  uintptr_t address;
  size_t len;
  /* User handler.  */
  sigsegv_area_handler_t handler;
  void *handler_arg;
}
node_t;

#define empty  ((node_t *) 0)
#define heightof(tree)  ((tree) == empty ? 0 : (tree)->height)
#define MAXHEIGHT  41

static void
rebalance (node_t ***nodeplaces_ptr, unsigned int count)
{
  if (count > 0)
    do
      {
        node_t **nodeplace = *--nodeplaces_ptr;
        node_t *node = *nodeplace;
        node_t *nodeleft = node->left;
        node_t *noderight = node->right;
        unsigned int heightleft = heightof (nodeleft);
        unsigned int heightright = heightof (noderight);
        if (heightright + 1 < heightleft)
          {
            node_t *nodeleftleft = nodeleft->left;
            node_t *nodeleftright = nodeleft->right;
            unsigned int heightleftright = heightof (nodeleftright);
            if (heightof (nodeleftleft) >= heightleftright)
              {
                node->left = nodeleftright; nodeleft->right = node;
                nodeleft->height = 1 + (node->height = 1 + heightleftright);
                *nodeplace = nodeleft;
              }
            else
              {
                nodeleft->right = nodeleftright->left;
                node->left = nodeleftright->right;
                nodeleftright->left = nodeleft;
                nodeleftright->right = node;
                nodeleft->height = node->height = heightleftright;
                nodeleftright->height = heightleft;
                *nodeplace = nodeleftright;
              }
          }
        else if (heightleft + 1 < heightright)
          {
            node_t *noderightright = noderight->right;
            node_t *noderightleft = noderight->left;
            unsigned int heightrightleft = heightof (noderightleft);
            if (heightof (noderightright) >= heightrightleft)
              {
                node->right = noderightleft; noderight->left = node;
                noderight->height = 1 + (node->height = 1 + heightrightleft);
                *nodeplace = noderight;
              }
            else
              {
                noderight->left = noderightleft->right;
                node->right = noderightleft->left;
                noderightleft->right = noderight;
                noderightleft->left = node;
                noderight->height = node->height = heightrightleft;
                noderightleft->height = heightright;
                *nodeplace = noderightleft;
              }
          }
        else
          {
            unsigned int height =
              (heightleft<heightright ? heightright : heightleft) + 1;
            if (height == node->height)
              break;
            node->height = height;
          }
      }
    while (--count > 0);
}

static node_t *
insert (node_t *new_node, node_t *tree)
{
  uintptr_t key = new_node->address;
  node_t **nodeplace = &tree;
  node_t **stack[MAXHEIGHT];
  unsigned int stack_count = 0;
  node_t ***stack_ptr = &stack[0];
  for (;;)
    {
      node_t *node = *nodeplace;
      if (node == empty)
        break;
      *stack_ptr++ = nodeplace; stack_count++;
      if (key < node->address)
        nodeplace = &node->left;
      else
        nodeplace = &node->right;
    }
  new_node->left = empty;
  new_node->right = empty;
  new_node->height = 1;
  *nodeplace = new_node;
  rebalance (stack_ptr, stack_count);
  return tree;
}

static node_t *
delete (node_t *node_to_delete, node_t *tree)
{
  uintptr_t key = node_to_delete->address;
  node_t **nodeplace = &tree;
  node_t **stack[MAXHEIGHT];
  unsigned int stack_count = 0;
  node_t ***stack_ptr = &stack[0];
  for (;;)
    {
      node_t *node = *nodeplace;
      if (node == empty)
        return tree;
      *stack_ptr++ = nodeplace; stack_count++;
      if (key == node->address)
        {
          if (node != node_to_delete)
            abort ();
          break;
        }
      if (key < node->address)
        nodeplace = &node->left;
      else
        nodeplace = &node->right;
    }
  {
    node_t **nodeplace_to_delete = nodeplace;
    if (node_to_delete->left == empty)
      {
        *nodeplace_to_delete = node_to_delete->right;
        stack_ptr--; stack_count--;
      }
    else
      {
        node_t ***stack_ptr_to_delete = stack_ptr;
        node_t **nodeplace = &node_to_delete->left;
        node_t *node;
        for (;;)
          {
            node = *nodeplace;
            if (node->right == empty)
              break;
            *stack_ptr++ = nodeplace; stack_count++;
            nodeplace = &node->right;
          }
        *nodeplace = node->left;
        node->left = node_to_delete->left;
        node->right = node_to_delete->right;
        node->height = node_to_delete->height;
        *nodeplace_to_delete = node;
        *stack_ptr_to_delete = &node->left;
      }
  }
  rebalance (stack_ptr, stack_count);
  return tree;
}

void
sigsegv_init (sigsegv_dispatcher *dispatcher)
{
  dispatcher->tree = empty;
}

void *
sigsegv_register (sigsegv_dispatcher *dispatcher,
                  void *address, size_t len,
                  sigsegv_area_handler_t handler, void *handler_arg)
{
  if (len == 0)
    return NULL;
  else
    {
      node_t *new_node = (node_t *) malloc (sizeof (node_t));
      new_node->address = (uintptr_t) address;
      new_node->len = len;
      new_node->handler = handler;
      new_node->handler_arg = handler_arg;
      dispatcher->tree = insert (new_node, (node_t *) dispatcher->tree);
      return new_node;
    }
}

void
sigsegv_unregister (sigsegv_dispatcher *dispatcher, void *ticket)
{
  if (ticket != NULL)
    {
      node_t *node_to_delete = (node_t *) ticket;
      dispatcher->tree = delete (node_to_delete, (node_t *) dispatcher->tree);
      free (node_to_delete);
    }
}

int
sigsegv_dispatch (sigsegv_dispatcher *dispatcher, void *fault_address)
{
  uintptr_t key = (uintptr_t) fault_address;
  node_t *tree = (node_t *) dispatcher->tree;
  for (;;)
    {
      if (tree == empty)
        return 0;
      if (key < tree->address)
        tree = tree->left;
      else if (key - tree->address >= tree->len)
        tree = tree->right;
      else
        break;
    }
  return (*tree->handler) (fault_address, tree->handler_arg);
}
