/* Copyright (c) 1999 Thorsten Kukuk
   Author: Thorsten Kukuk <kukuk@suse.de>

   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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#define _GNU_SOURCE

#include <assert.h>
#include <string.h>
#include <locale.h>
#include <libintl.h>

#include "nisd.h"
#include "nis_xdr.h"
#include "log_msg.h"

#ifndef _
#define _(String) gettext (String)
#endif

/*
 * This module implements a generic object cache. This is
 * _NOT_ the same thing as the cache manager. We keep this cache
 * in order to make the server's life easier and that's it.
 *
 * The most important routines exported by this module are
 * nis_dcache_lookup() and nis_ocache_lookup(). The idea is
 * to use these within the server instead of nis_lookup(). This
 * allows us to cache the result returned by nis_lookup() so that
 * we can avoid having to to a full nis_lookup() again the next
 * time we need the same object.
 *
 * Notes:
 *
 * - We don't cache entry objects. There's no point. We may cache
 *   table objects however.
 *
 * - We have both dcache and ocache lookup routines: the former returns
 *   only a directory object without any other cruft surrounding it
 *   and uses somewhat different sanity checks. The latter returns
 *   the entire nis_object structure.
 *
 * - All objects returned by this module must be freed, or we couldn't
 *   make it threadsafe.
 */

#define TABLESIZE 256
#define HASH_MASK 0x000000FF

#define I_SERVE 0x01
#define I_AM_MASTER 0x02

struct ocache_t {
  time_t c_time;                /* Creation time */
  nis_object *obj;              /* object */
  uint32_t hval;                /* Hash key */
  int ostat;                    /* Status bits */
  struct ocache_t *next;      /* Collision pointer */
};

static struct ocache_t *ocache[TABLESIZE];
static unsigned long o_calls = 0, o_hits = 0, o_miss = 0;
static unsigned long d_calls = 0, d_hits = 0, d_miss = 0;

void
get_ocache_stats (u_long *hits, u_long *misses, u_long *calls)
{
  *hits = o_hits;
  *misses = o_miss;
  *calls = o_calls;
}

void
get_dcache_stats (u_long *hits, u_long *misses, u_long *calls)
{
  *hits = d_hits;
  *misses = d_miss;
  *calls = d_calls;
}

static struct ocache_t *
ocache_search (const_nis_name name)
{
  struct ocache_t *cur;
  uint32_t hash;

  assert (name);

  hash = __nis_hash (name, strlen (name));
  cur = ocache[hash & HASH_MASK];

  while(cur != NULL)
    {
      if (cur->hval == hash)
        return cur;
      cur = cur->next;
    }
  return NULL;
}

static void
ocache_delete (const_nis_name name)
{
  struct ocache_t *cur, *prev;
  uint32_t hash;

  assert (name);

  hash = __nis_hash (name, strlen (name));
  cur = ocache[hash & HASH_MASK];
  prev = NULL;

  while(cur != NULL)
    {
      if (cur->hval == hash)
        break;
      prev = cur;
      cur = cur->next;
    }

  if (cur == NULL)
    return;

  if (cur == ocache[hash & HASH_MASK] || prev == ocache[hash & HASH_MASK])
    ocache[hash & HASH_MASK] = cur->next;
  else
    prev = cur->next;

  xdr_free ((xdrproc_t) xdr_nis_object, (caddr_t)cur->obj);
  free (cur->obj);
  free (cur);

  return;
}

nis_error
nis_ocache_lookup (nis_name name, uint32_t flags, uint32_t lookup_flags,
		   nis_object **obj)
{
  nis_result *res;
  struct ocache_t *o_ent;

  /* See if the directory is in the cache. */
  if (!(flags & LOOKUP_ONLY))
    {
      o_ent = ocache_search (name);
      ++o_calls;
      if (o_ent == NULL)
	++o_miss;
      else
	{
	  ++o_hits;
	  if ((u_long)time (NULL) >
	      ((u_long)o_ent->c_time + o_ent->obj->zo_ttl))
	    {
	      /* TTL is expired */
	      *obj = NULL;
	      ocache_delete (name);
	      if (flags & CACHE_ONLY)
		return NIS_CACHEEXPIRED;
	    }
	  else
	    {
	      /* TTL is not expired */
	      if ((flags & CHECK_MASTER) && !(o_ent->ostat & I_AM_MASTER))
		return NIS_NOTMASTER;

	      if ((flags & CHECK_SERVER) && !(o_ent->ostat & I_SERVE))
		return NIS_NOT_ME;

	      if (obj)
		*obj = nis_clone_object (o_ent->obj, NULL);

	      return NIS_SUCCESS;
	    }
	}
    }

  if (flags & CACHE_ONLY)
    return NIS_NOTFOUND;

  /* our own nisd_lookup will add the object */
  res = nisd_lookup (name, lookup_flags);
  if (res == NULL)
    return NIS_NOMEMORY;

  if (res->status != NIS_SUCCESS)
    {
      nis_error oerr = res->status;
      nis_freeresult (res);
      return oerr;
    }

  nis_freeresult (res);

  o_ent = ocache_search (name);
  if (o_ent == NULL)
    return NIS_NOTFOUND;

  if (flags & CHECK_MASTER && !(o_ent->ostat & I_AM_MASTER))
    return NIS_NOTMASTER;

  if (flags & CHECK_SERVER && !(o_ent->ostat & I_SERVE))
    return NIS_NOT_ME;

  if (obj)
    *obj = nis_clone_object (o_ent->obj, NULL);

  return NIS_SUCCESS;
}

void
nis_ocache_insert (nis_object *obj)
{
  struct ocache_t *new;
  uint32_t hkey;
  char *buffer, *cp;

  assert (obj);
  assert (obj->zo_name);
  assert (obj->zo_domain);

  buffer = alloca (strlen (obj->zo_name) + strlen (obj->zo_domain) + 2);
  cp = stpcpy (buffer, obj->zo_name);
  *cp++ = '.';
  strcpy (cp, obj->zo_domain);

  printf ("%s: %s.%s", buffer, obj->zo_name, obj->zo_domain);

  if (verbose)
    log_msg (LOG_DEBUG, _("inserting %s into cache"), buffer);

  if (ocache_search (buffer) != NULL)
    ocache_delete (buffer);

  hkey = __nis_hash (buffer, strlen (buffer));

  new = calloc (1, sizeof(struct ocache_t));
  new->hval = hkey;
  new->obj = nis_clone_object (obj, NULL);
  new->c_time = time (NULL);
  new->next = NULL;

  if (__type_of(new->obj) == DIRECTORY_OBJ)
    {
      if (nis_dir_cmp (new->obj->DI_data.do_servers.do_servers_val[0].name, nis_local_host ()) == SAME_NAME)
	new->ostat |= I_AM_MASTER;
      if (is_in_srvlist (obj->zo_domain) == TRUE)
	new->ostat |= I_SERVE;
    }
  else
    {
      directory_obj *dir;

      if (nis_dcache_lookup (obj->zo_domain, 0, 0, &dir) != NIS_SUCCESS)
	log_msg (LOG_DEBUG, _("warning: couldn't determine object dir for %s"),
		 obj->zo_domain);
      else
	{
	  if (nis_dir_cmp (dir->do_servers.do_servers_val[0].name, nis_local_host ()) == SAME_NAME)
	    new->ostat |= I_AM_MASTER;
	  if (is_in_srvlist (obj->zo_domain) == TRUE)
	    new->ostat |= I_SERVE;
	}
    }

  new->next = ocache[hkey & HASH_MASK];
  ocache[hkey & HASH_MASK] = new;
}

/*
 * Meaning of special cache lookup flags:
 *
 * CHECK_MASTER         Make sure I am the master server, return
 *                      NIS_NOTMASTER otherwise.
 * CHECK_SERVER         Make sure I am a server (master or replica),
 *                      return NIS_NOT_ME otherwise.
 * CACHE_ONLY           Check the cache only; don't rollover to
 *                      nis_lookup().
 * LOOKUP_ONLY          Opposite of CACHE_ONLY: do an nis_lookup()
 *                      and refresh the cache on success (but don't
 *                      check the cache for lookup matches).
 */
nis_error
nis_dcache_lookup (nis_name name, uint32_t flags, uint32_t lookup_flags,
		   directory_obj **dir)
{
  nis_result *res;
  struct ocache_t *d_ent;

  /* See if the directory is in the cache. */
  if (!(flags & LOOKUP_ONLY))
    {
      d_ent = ocache_search (name);
      ++d_calls;
      if (d_ent == NULL)
	++d_miss;
      else
	{
	  ++d_hits;
	  if (__type_of(d_ent->obj) != DIRECTORY_OBJ)
	    return NIS_BADNAME;
	  if ((u_long)time (NULL) >
	      ((u_long)d_ent->c_time + d_ent->obj->zo_ttl))
	    {
	      /* TTL is expired */
	      *dir = NULL;
	      ocache_delete (name);
	      if (flags & CACHE_ONLY)
		return NIS_CACHEEXPIRED;
	    }
          else
            {
              /* TTL is not expired */
	      if ((flags & CHECK_MASTER) && !(d_ent->ostat & I_AM_MASTER))
		return NIS_NOTMASTER;

	      if ((flags & CHECK_SERVER) && !(d_ent->ostat & I_SERVE))
		return NIS_NOT_ME;

	      if (dir)
		*dir = &d_ent->obj->DI_data;

	      return NIS_SUCCESS;
	    }
	}
    }

  if (flags & CACHE_ONLY)
    return NIS_NOTFOUND;

  /* our own nisd_lookup will add the object */
  res = nisd_lookup (name, lookup_flags);
  if (res == NULL)
    return NIS_NOMEMORY;

  if (res->status != NIS_SUCCESS)
    {
      nis_error err = res->status;
      nis_freeresult (res);
      return err;
    }

  if (NIS_RES_NUMOBJ(res) > 1 ||
      __type_of(NIS_RES_OBJECT(res)) != DIRECTORY_OBJ)
    {
      nis_freeresult (res);
      return NIS_BADNAME;
    }

  nis_freeresult (res);

  d_ent = ocache_search (name);

  if (flags & CHECK_MASTER && !(d_ent->ostat & I_AM_MASTER))
    return NIS_NOTMASTER;

  if (flags & CHECK_SERVER && !(d_ent->ostat & I_SERVE))
    return NIS_NOT_ME;

  if (dir)
    *dir = &d_ent->obj->DI_data;

  return NIS_SUCCESS;
}

void
remove_from_cache (const_nis_name name)
{
  ocache_delete (name);
  if (verbose)
    log_msg (LOG_DEBUG, _("flushing %s from cache"), name);

#if 0 /* XXX We don't modify the root object in the moment */
  /*
   * If the domain being flushed is our local domain, then
   * we need to update the cold start object. If the cache
   * manager is running then it will try to do it too, but
   * we do it anyway in case that fails.
   *
   * Do we need to tell the nis_cachemgr that there is a new
   * cold start file ?
   */
  if (nis_dir_cmp (name, nis_local_directory ()) == SAME_NAME)
    {
      nis_update_cold ();
      ocache_delete (name);
    }
#endif

  return;
}
