/* --------------------------------------------------------------------------

   xdb_file module ( threaded environment )

   Author: Łukasz Karwacki <lukasm@wp-sa.pl>

 * --------------------------------------------------------------------------*/
 
#include <jabberd.h>

//default value for hash 
#define FILES_PRIME 509

typedef struct cacher_struct
{
    WPHASH_BUCKET;
    volatile xmlnode file;
    unsigned long lasttime; //last access time
    struct cacher_struct *anext,*aprev;
    volatile int ref; //number of references to hash element
} *cacher, _cacher;

typedef struct xdbf_struct
{
    int shutdown;
    char *spool;
    instance i;
    int timeout;    //hash elements timeout 
    HASHTABLE hash;
    int sizelimit;  //sizelimit for set
    SEM_VAR hash_sem;
    cacher last;    //oldest element
    cacher first;   //latest element
    unsigned long actualtime;
} *xdbf, _xdbf;

/*
          | aprev    c  anext |   | aprev   c  anext |
           xf->first ^                      ^  xf->last
*/

inline void cacher_add_element(xdbf xf,cacher c) {
  /* first element */
  if (xf->last == NULL)
	xf->last = c;
  else
	xf->first->aprev = c;

  c->anext = xf->first;
  xf->first = c;
}


inline void cacher_remove_element(xdbf xf, cacher c) {

  if (c->aprev) {
	c->aprev->anext=c->anext;
  }
  if (c->anext) {
	c->anext->aprev=c->aprev;
  }
  if (xf->first == c) {
	xf->first = c->anext;
  }
  if (xf->last == c) {
	xf->last = c->aprev;
  }
  c->anext = c->aprev = NULL;
  return;

}

inline void cacher_touch_element(xdbf xf,cacher c) {
  /* is it add */
  if ( (c->aprev != NULL) || (c->anext != NULL) || (xf->first==c) ) {
	cacher_remove_element(xf,c);
  }
  cacher_add_element(xf,c);
}

/* remove some elements */
result xdb_file_purge(void *arg)
{
    xdbf xf = (xdbf)arg;
	cacher c;
	cacher t;

	log_debug(ZONE,"check hash");

	SEM_LOCK(xf->hash_sem);
    xf->actualtime = time(NULL);

	if ((xf->last == NULL)||(xf->shutdown)) {
	  SEM_UNLOCK(xf->hash_sem);
	  return r_DONE;
	}

	t = NULL;

	/* loop till c and data are valid */
	for (c = xf->last; c != NULL;) {

	  /* if someone is using this */
	  if (c->ref) break;
	  if (c->lasttime > xf->actualtime - xf->timeout) break;

	  /* no ref and old */

	  t = c;
	  
	  c = t->aprev;

	  t->aprev = NULL;
	  t->anext = NULL;
	  
	  /* remove */
	  wpxhash_zap(xf->hash,t->wpkey);

	  /* free */
	  xmlnode_free(t->file);
	}

	/* t elem is free , c is last one */

	/* if something was removed */
	if (t) {
	  /* if not everythink to remove */
	  if (c) {
		xf->last = c;
		c->anext = NULL;
	  }
	  else {
		xf->first = NULL;
		xf->last = NULL;
	  }	  
	}
	SEM_UNLOCK(xf->hash_sem);
    return r_DONE;
}


/* extern */
char *xdb_file_full(int create, pool p, char *spl, char *host, char *file, char *ext);
xmlnode xdb_file_parse(char *file,pool p);
int xdb_file2file(char * file, xmlnode node, int maxsize);

/* the callback to handle xdb packets */
result xdb_file_phandler(instance i, dpacket dp, void *arg)
{
    char *full, *ns;
    xdbf xf = (xdbf)arg;
    xmlnode file, top, data;
    int flag_set = 0;
    cacher c; //element
	int remove = 0;

    log_debug(ZONE,"handling xdb request %s",xmlnode2str(dp->x));
        
    if((ns = xmlnode_get_attrib(dp->x,"ns")) == NULL)
        return r_ERR;

    if(j_strcmp(xmlnode_get_attrib(dp->x,"type"), "set") == 0)
        flag_set = 1;

    /* is this request specific to a user or global data? use that for the file name */
    if(dp->id->user != NULL)
    {
        full = xdb_file_full(flag_set, dp->p, xf->spool, 
							 dp->id->server, dp->id->user, "xml");
    }
    else
	{
		full = xdb_file_full(flag_set, dp->p, xf->spool, 
							 dp->id->server, "global", "xdb");
    }

    if(full == NULL) {
	  /* return data if get */
	  if (flag_set == 0) {
		xmlnode_put_attrib(dp->x,"type","result");
		xmlnode_put_attrib(dp->x,"to",xmlnode_get_attrib(dp->x,"from"));
		xmlnode_put_attrib(dp->x,"from",jid_full(dp->id));
		deliver(dpacket_new(dp->x), NULL);
		return r_DONE;
	  }

	  return r_ERR;
	}
	
	log_debug(ZONE,"%s",full);

    /* =================================  S E T  ===================================*/
    if (flag_set == 1)  {
	  char *act,*match;
	  int ret = 1;

	  SEM_LOCK(xf->hash_sem);
	  if((c = wpxhash_get(xf->hash,full)) != NULL)  {
		/* if no data */
		if (c->file == NULL) {
		  c->ref++;
		  
		  /* wait for data, I don't like this :( */
		  SEM_UNLOCK(xf->hash_sem);
		  
		  while (1) {
			usleep(15);
			SEM_LOCK(xf->hash_sem);
			if (c->file != NULL) break;
			SEM_UNLOCK(xf->hash_sem);
		  }
		}
		else
		  c->ref++;

		/* data take */
		top = c->file;

		/* set data are busy */
		c->file = NULL;
		
		/* hash free */
		SEM_UNLOCK(xf->hash_sem);
	  }
	  else 	{	
		pool p = pool_heap(1024);
		c = pmalloco(p,sizeof(_cacher));
		c->ref = 1;

		wpxhash_put(xf->hash,pstrdup(p,full),c);

		/* unlock hash */	   
		SEM_UNLOCK(xf->hash_sem);
		
		/* parse file */
		top = xdb_file_parse(c->wpkey,p);
		  
		if(top == NULL)
		  top = xmlnode_new_tag_pool(p,"xdb");		
	  }

	  // ++ top has data and c->file = NULL and sem if unlocked here 
	  file = top;
	  
	  /* if we're dealing w/ a resource, just get that element */		
	  if(dp->id->resource != NULL) {
		if (
			(top = xmlnode_get_tag(top,spools(dp->p,"res?id=",dp->id->resource,dp->p))) == NULL
			) {
		  top = xmlnode_insert_tag(file,"res");
		  xmlnode_put_attrib(top,"id",dp->id->resource);
		}
	  }

	  /* data from xdb */
	  data = xmlnode_get_tag(top,spools(dp->p,"?xdbns=",ns,dp->p));
	  
	  act = xmlnode_get_attrib(dp->x,"action");
	  match = xmlnode_get_attrib(dp->x,"match");
	  
	  if (act != NULL)    {
		switch(*act)
		  {
		  case 'i':
			/* we're inserting into something that doesn't exist?!?!? */
			if(data == NULL) { 
			  data = xmlnode_insert_tag(top,"foo");
			  xmlnode_put_attrib(data,"xdbns",ns);
			  xmlnode_put_attrib(data,"xmlns",ns); /* should have a top-level xmlns attrib */
			}
			xmlnode_hide(xmlnode_get_tag(data,match)); /* any match is a goner */
			
			xmlnode_insert_tag_node(data, xmlnode_get_firstchild(dp->x));		    
			break;

		  case 'c': /* check action */
			if(match != NULL)
			  data = xmlnode_get_tag(data,match);
			if(j_strcmp(xmlnode_get_data(data),
						xmlnode_get_data(xmlnode_get_firstchild(dp->x))) != 0) {
			  log_debug(ZONE,"xdb check action returning error to signify unsuccessful check");
			  ret = 0;
			}
			flag_set = 0;
			break;

		  default:
			log_warn("xdb_file","unable to handle unknown xdb action '%s'",act);
			ret = 0;
			break;
		  }
	  }
	  else {
		/* overwrite */
		if(data != NULL)
		  xmlnode_hide(data);
		
		/* copy the new data into file */
		data = xmlnode_insert_tag_node(top, xmlnode_get_firstchild(dp->x));
		xmlnode_put_attrib(data,"xdbns",ns);
	  }
	  	
	  /* if ready to send result */
	  if (ret)  {	    	
		
		if (flag_set) {
		  int result = xdb_file2file(c->wpkey,file,xf->sizelimit);
		  
		  if (result < 0) {
			log_error(dp->id->server,"xdb request failed, unable to save to file %s",
					  c->wpkey);
			ret = 0;
		  }
		  else if (result == 0) {
			/* size reached */
			ret = 0;
		  }
		}
		
		if (ret) {			    
		  xmlnode_put_attrib(dp->x,"type","result");
		  xmlnode_put_attrib(dp->x,"to",xmlnode_get_attrib(dp->x,"from"));
		  xmlnode_put_attrib(dp->x,"from",jid_full(dp->id));
		  deliver(dpacket_new(dp->x), NULL); 
		}
	  }
	  
	  SEM_LOCK(xf->hash_sem);
	  
	  /* our ref-- */
	  c->ref--;
	  
	  /* if nobody waits data always remove */
	  if (c->ref == 0) {
		wpxhash_zap(xf->hash,c->wpkey);
		remove = 1;
		if (xf->timeout > 0) {
		  cacher_remove_element(xf,c);
		}
	  }
	  else {
		/* if someone waits data, give data */
		c->file = file;
	  }
	  
	  SEM_UNLOCK(xf->hash_sem);
	  
	  if (remove)
		xmlnode_free(file);
	  
	  if(ret)
		return r_DONE;
	  else
		return r_ERR;
	}
    /* =================================  G E T  ===================================*/   
    else {
	  /* 
         If element exist get data or wait for them
		 If no element, create element, get data, update element 
	  */		       
	  SEM_LOCK(xf->hash_sem);
	  if((c = wpxhash_get(xf->hash,full)) != NULL)  {		
		/* if no data */
		if (c->file == NULL) {
		  c->ref++;
		  
		  /* wait data, I don't like this :( */
		  SEM_UNLOCK(xf->hash_sem);
		  
		  while (1) {
			usleep(15);
			
			SEM_LOCK(xf->hash_sem);
			if (c->file != NULL) break;
			SEM_UNLOCK(xf->hash_sem);
		  }		
  
		  c->ref--;

		  /* if no cache and nobody waits data , remove */
		  if ((xf->timeout == 0)&&(c->ref==0)) {
			wpxhash_zap(xf->hash,c->wpkey);
			remove = 1;
		  }
		  else {
			if (xf->timeout > 0) {
			  c->lasttime = xf->actualtime;
			  cacher_touch_element(xf,c);
			}
		  }
		}
		else {
		  if (xf->timeout > 0) {
			c->lasttime = xf->actualtime;
			cacher_touch_element(xf,c);
		  }
		}

		/* has data */
		top = c->file;
	  }
	  else 	{	
		pool p = pool_heap(1024);
		c = pmalloco(p,sizeof(_cacher));
		c->ref = 1;

		wpxhash_put(xf->hash,pstrdup(p,full),c);

		/* unlock hash */	   
		SEM_UNLOCK(xf->hash_sem);
		
		log_debug(ZONE,"get new %s",c->wpkey);		
		
		/* parse file */
		top = xdb_file_parse(c->wpkey,p);
		  
		if(top == NULL)
		  top = xmlnode_new_tag_pool(p,"xdb");		
		
		log_debug(ZONE,"parse: %s",xmlnode2str(top));
		
		/* lock */
		SEM_LOCK(xf->hash_sem);

		/* ref-- */
		c->ref--;

		/* if no cache and nobody waits data , remove */
		if ((xf->timeout == 0)&&(c->ref==0)) {
		  /* remove from hash */		  
		  wpxhash_zap(xf->hash,c->wpkey);
		  remove = 1;
		}
		else {
		  c->file = top;
		  if (xf->timeout > 0) {
			c->lasttime = xf->actualtime;
			cacher_touch_element(xf,c);
		  }
		}
	  }
	  // ++ top has data and sem hash is locked here

	  file = top;

	  /* if we're dealing w/ a resource, just get that element */		
	  if(dp->id->resource != NULL) {
		if (
			(top = xmlnode_get_tag(top,spools(dp->p,"res?id=",dp->id->resource,dp->p))) == NULL) {
		  top = xmlnode_insert_tag(file,"res");
		  xmlnode_put_attrib(top,"id",dp->id->resource);
		}
	  }
	  
	  /* just query the relevant namespace */
	  data = xmlnode_get_tag(top,spools(dp->p,"?xdbns=",ns,dp->p));
	  
	  if(data != NULL) { /* cool, send em back a copy of the data */
		xmlnode_hide_attrib(xmlnode_insert_tag_node(dp->x, data),"xdbns");
	  }

	  /* unlock hash */
	  SEM_UNLOCK(xf->hash_sem);
	  	  
	  xmlnode_put_attrib(dp->x,"type","result");
	  xmlnode_put_attrib(dp->x,"to",xmlnode_get_attrib(dp->x,"from"));
	  xmlnode_put_attrib(dp->x,"from",jid_full(dp->id));
	  deliver(dpacket_new(dp->x), NULL);
	  
	  if (remove)
		xmlnode_free(file);	

	  return r_DONE;
    }
}

void xdb_file_cleanup(void *arg)
{
  xdbf xf = (xdbf)arg;

  SEM_LOCK(xf->hash_sem);     
  xf->shutdown = 1;
  /* hash walk and free all elements, but why ?? */
  SEM_UNLOCK(xf->hash_sem);
}

void xdb_file(instance i, xmlnode x)
{
    char *spl,*to;
    xmlnode config;
    xdbcache xc;
    xdbf xf;
    int timeout = 0; /* defaults to timeout forever */
    int sizelimit = 0; /* no size limit */

    log_debug(ZONE,"xdb_file loading");

    xc = xdb_cache(i);
    config = xdb_get(xc, jid_new(xmlnode_pool(x),"config@-internal"),"jabber:config:xdb_file");

    spl = xmlnode_get_tag_data(config,"spool");
    if(spl == NULL)
    {
        log_error(NULL,"xdb_file: No filesystem spool location configured");
        return;
    }
    to = xmlnode_get_tag_data(config,"timeout");
    if(to != NULL)
        timeout = atoi(to);

    to = xmlnode_get_tag_data(config,"sizelimit");
    if(to != NULL)
        sizelimit = atoi(to);

    xf = pmalloco(i->p,sizeof(_xdbf));
    xf->spool = pstrdup(i->p,spl);
	xf->shutdown = 0;
    xf->timeout = timeout;
    xf->sizelimit = sizelimit;
    xf->i = i;
    xf->actualtime = time(NULL);
	xf->last = NULL;  //last element in list, start removing from here
	xf->first = NULL; //first element in list

    SEM_INIT(xf->hash_sem);
	
    xf->hash = wpxhash_new(j_atoi(xmlnode_get_tag_data(config,"maxfiles"),FILES_PRIME));
	
    log_debug(ZONE,"Starting XDB size limit =  %d, timeout = %d",sizelimit,timeout);
	
    register_phandler(i, o_DELIVER, xdb_file_phandler, (void *)xf);

    if(timeout > 0) /* 0 is expired immediately, -1 is cached forever */
       register_beat(10, xdb_file_purge, (void *)xf); 

    xmlnode_free(config);
    pool_cleanup(i->p, xdb_file_cleanup, (void*)xf);
}
