//+++2003-11-18
//    Copyright (C) 2001,2002,2003  Mike Rieker, Beverly, MA USA
//
//    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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//---2003-11-18

/************************************************************************/
/*									*/
/*  This is a platform independent timer driver				*/
/*									*/
/*  It calls platform specific routines to perform the basic timing 	*/
/*  functions								*/
/*									*/
/************************************************************************/

#define _OZ_DEV_TIMER_C
#include "ozone.h"
#include "oz_dev_timer.h"
#include "oz_io_timer.h"
#include "oz_knl_hw.h"
#include "oz_knl_kmalloc.h"
#include "oz_knl_lowipl.h"
#include "oz_knl_objtype.h"
#include "oz_knl_printk.h"
#include "oz_knl_sdata.h"
#include "oz_knl_status.h"
#include "oz_knl_thread.h"

struct OZ_Timer { OZ_Objtype objtype;
                  OZ_Timer *next;
                  OZ_Datebin datebin;
                  void (*(entry)) (void *param, OZ_Timer *timer);
                  void *param;
                };

typedef struct Chnex Chnex;
typedef struct Iopex Iopex;

struct Iopex { Iopex *next;
               Iopex **prev;
               OZ_Ioop *ioop;
               OZ_Timer timer;
             };

struct Chnex { Iopex *queue;
             };
                 
static uLong oz_dev_timer_assign (OZ_Devunit *devunit, void *devexv, OZ_Iochan *iochan, void *chnexv, OZ_Procmode procmode);
static void oz_dev_timer_abort (OZ_Devunit *devunit, void *devexv, OZ_Iochan *iochan, void *chnexv, OZ_Ioop *ioop, void *iopexv, OZ_Procmode procmode);
static uLong oz_dev_timer_start (OZ_Devunit *devunit, void *devexv, OZ_Iochan *iochan, void *chnex, OZ_Procmode procmode,
                                 OZ_Ioop *ioop, void *iopexv, uLong funcode, uLong as, void *ap);

static const OZ_Devfunc oz_dev_timer_functable = { 0, sizeof (Chnex), sizeof (Iopex), 0, NULL, NULL, NULL, oz_dev_timer_assign, NULL, oz_dev_timer_abort, oz_dev_timer_start, NULL };

static int initialized = 0;
static OZ_Devclass *devclass;
static OZ_Devdriver *devdriver;
static OZ_Devunit *devunit;

static OZ_Timer *alltimers = NULL;
static OZ_Lowipl *timer_lowipl = NULL;

static Long intserv_entered = 0;
static Long intserv_exited  = 0;
static Long lowipl_entered  = 0;
static Long lowipl_exited   = 0;

static void iotimerisup (void *ioopv, OZ_Timer *timer);
static void timer_call (void *dummy, OZ_Lowipl *lowipl);

/************************************************************************/
/*									*/
/*  Boot-time initialization routine					*/
/*									*/
/************************************************************************/

void oz_dev_timer_init (void)

{
  if (!initialized) {
    oz_knl_printk ("oz_dev_timer_init\n");
    initialized  = 1;
    devclass     = oz_knl_devclass_create (OZ_IO_TIMER_CLASSNAME, OZ_IO_TIMER_BASE, OZ_IO_TIMER_MASK, "oz_dev_timer");
    devdriver    = oz_knl_devdriver_create (devclass, "oz_dev_timer");
    devunit      = oz_knl_devunit_create (devdriver, "timer", "generic timer", &oz_dev_timer_functable, 0, oz_s_secattr_tempdev);
    timer_lowipl = oz_knl_lowipl_alloc ();
    oz_knl_thread_timerinit ();
  }
}

/************************************************************************/
/*									*/
/*  An new channel is being assigned - clear the queue pointer		*/
/*									*/
/************************************************************************/

static uLong oz_dev_timer_assign (OZ_Devunit *devunit, void *devexv, OZ_Iochan *iochan, void *chnexv, OZ_Procmode procmode)

{
  ((Chnex *)chnexv) -> queue = NULL;
  return (OZ_SUCCESS);
}

/************************************************************************/
/*									*/
/*  Abort an timer I/O request						*/
/*									*/
/************************************************************************/

static void oz_dev_timer_abort (OZ_Devunit *devunit, void *devexv, OZ_Iochan *iochan, void *chnexv, OZ_Ioop *ioop, void *iopexv, OZ_Procmode procmode)

{
  Chnex *chnex;
  Iopex *aborted, *iopex, **liopex, *niopex;
  uLong tm;

  chnex   = chnexv;
  aborted = NULL;
  tm = oz_hw_smplock_wait (oz_hw_smplock_tm);	/* lock database */
  for (liopex = &(chnex -> queue); (iopex = *liopex) != NULL;) {
    if (!oz_knl_ioabortok (iopex -> ioop, iochan, procmode, ioop) || !oz_knl_timer_remove (&(iopex -> timer))) {
      liopex = &(iopex -> next);		/* leave it as is, on to next */
    } else {
      niopex = iopex -> next;			/* unlink from chnex -> queue */
      if (niopex != NULL) niopex -> prev = liopex;
      *liopex = niopex;
      iopex -> next = aborted;			/* link to aborted */
      aborted = iopex;
    }
  }
  oz_hw_smplock_clr (oz_hw_smplock_tm, tm);	/* release database */

  while ((iopex = aborted) != NULL) {		/* abort all we found */
    aborted = iopex -> next;
    oz_knl_iodone (iopex -> ioop, OZ_ABORTED, NULL, NULL, NULL);
  }
}

/************************************************************************/
/*									*/
/*  Start performing a timer i/o function				*/
/*									*/
/************************************************************************/

static uLong oz_dev_timer_start (OZ_Devunit *devunit, void *devexv, OZ_Iochan *iochan, void *chnexv, OZ_Procmode procmode,
                                 OZ_Ioop *ioop, void *iopexv, uLong funcode, uLong as, void *ap)

{
  Chnex *chnex;
  Iopex *iopex;
  uLong tm;
  OZ_IO_timer_waituntil timer_waituntil;

  chnex = chnexv;
  iopex = iopexv;

  switch (funcode) {
    case OZ_IO_TIMER_WAITUNTIL: {
      movc4 (as, ap, sizeof timer_waituntil, &timer_waituntil);
      iopex -> timer.objtype = OZ_OBJTYPE_TIMER;
      iopex -> timer.next    = &(iopex -> timer);
      iopex -> ioop = ioop;
      tm = oz_hw_smplock_wait (oz_hw_smplock_tm);
      iopex -> next = chnex -> queue;
      iopex -> prev = &(chnex -> queue);
      if (iopex -> next != NULL) iopex -> next -> prev = &(iopex -> next);
      chnex -> queue = iopex;
      oz_knl_timer_insert (&(iopex -> timer), timer_waituntil.datebin, iotimerisup, iopex);
      oz_hw_smplock_clr (oz_hw_smplock_tm, tm);
      return (OZ_STARTED);
    }

    default: {
      return (OZ_BADIOFUNC);
    }
  }
}

/* An I/O style timer has expired, call the completion routine */

static void iotimerisup (void *iopexv, OZ_Timer *timer)

{
  Iopex *iopex, *next, **prev;
  uLong tm;

  iopex = iopexv;

  tm = oz_hw_smplock_wait (oz_hw_smplock_tm);	/* lock database */
  next = iopex -> next;				/* remove from chnex -> queue */
  prev = iopex -> prev;
  if (next != NULL) next -> prev = prev;
  *prev = next;
  oz_hw_smplock_clr (oz_hw_smplock_tm, tm);	/* release database */

  oz_knl_iodone (iopex -> ioop, OZ_SUCCESS, NULL, NULL, NULL);
}

/************************************************************************/
/*									*/
/*  Allocate a timer struct for later use				*/
/*									*/
/*    Output:								*/
/*									*/
/*	oz_knl_timer_alloc = pointer to timer struct			*/
/*	              NULL = npp quota exceeded				*/
/*									*/
/************************************************************************/

OZ_Timer *oz_knl_timer_alloc (void)

{
  OZ_Timer *timer;

  /* Create a new timer queue entry */

  timer = OZ_KNL_NPPMALLOQ (sizeof *timer);
  if (timer != NULL) {
    timer -> objtype = OZ_OBJTYPE_TIMER;
    timer -> next    = timer;
    timer -> entry   = NULL;
    timer -> param   = NULL;
  }

  return (timer);
}

/************************************************************************/
/*									*/
/*  Insert a timer request in queue					*/
/*									*/
/*    Input:								*/
/*									*/
/*	timer   = timer struct allocated by oz_knl_timer_alloc		*/
/*	datebin = date/time the timer is to expire			*/
/*	entry   = callback routine entrypoint				*/
/*	param   = callback routine parameter				*/
/*									*/
/*	smplock <= tm							*/
/*									*/
/*    Output:								*/
/*									*/
/*	timer request queued, callback will happen at or just after 	*/
/*	given datebin							*/
/*									*/
/************************************************************************/

void oz_knl_timer_insert (OZ_Timer *timer, OZ_Datebin datebin, void (*entry) (void *param, OZ_Timer *timer), void *param)

{
  uLong tm;
  OZ_Timer **ltimer, *xtimer;

  OZ_KNL_CHKOBJTYPE (timer, OZ_OBJTYPE_TIMER);

  /* Lock timer queue */

  tm = oz_hw_smplock_wait (oz_hw_smplock_tm);
  oz_knl_timer_validate ();

  /* Make sure it is not in current queue */

  if (timer -> next != timer) {
    if (!oz_knl_timer_remove (timer)) oz_crash ("oz_knl_timer_insert: couldnt remove entry from queue");
  }

  /* Now that is is not in the queue, save the parameters */

  timer -> datebin = datebin;
  timer -> entry   = entry;
  timer -> param   = param;

  /* Insert new entry sorted by ascending datebin */

  for (ltimer = &alltimers; (xtimer = *ltimer) != NULL; ltimer = &(xtimer -> next)) {
    if (OZ_HW_DATEBIN_CMP (xtimer -> datebin, timer -> datebin) > 0) break;
  }
  *ltimer = timer;
  timer -> next = xtimer;

  /* If this one is the new top, set the next event's datebin = this timer's datebin */

  if (timer == alltimers) oz_hw_timer_setevent (timer -> datebin);

  /* Release lock */

  oz_knl_timer_validate ();
  oz_hw_smplock_clr (oz_hw_smplock_tm, tm);
}

/************************************************************************/
/*									*/
/*  Remove timer request from timer queue				*/
/*									*/
/*    Input:								*/
/*									*/
/*	timer = request to be removed					*/
/*									*/
/*    Output:								*/
/*									*/
/*	oz_knl_timer_remove = 0 : request was not in queue		*/
/*	                      1 : request was in queue and removed	*/
/*									*/
/************************************************************************/

int oz_knl_timer_remove (OZ_Timer *timer)

{
  int rc;
  uLong tm;
  OZ_Timer **ltimer, *xtimer;

  OZ_KNL_CHKOBJTYPE (timer, OZ_OBJTYPE_TIMER);

  /* Lock the timer queue */

  tm = oz_hw_smplock_wait (oz_hw_smplock_tm);
  oz_knl_timer_validate ();

  /* Scan queue for given entry.  If found, remove it. */

  rc = 0;
  for (ltimer = &alltimers; (xtimer = *ltimer) != NULL; ltimer = &(xtimer -> next)) {
    if (xtimer == timer) {
      *ltimer = xtimer -> next;
      xtimer -> next = xtimer;
      rc = 1;
      break;
    }
  }

  /* Release lock and return 'removed from queue' flag */

  oz_knl_timer_validate ();
  oz_hw_smplock_clr (oz_hw_smplock_tm, tm);
  return (rc);
}

/************************************************************************/
/*									*/
/*  Free off a timer struct						*/
/*									*/
/*    Input:								*/
/*									*/
/*	timer = pointer to timer struct to free				*/
/*									*/
/************************************************************************/

void oz_knl_timer_free (OZ_Timer *timer)

{
  OZ_KNL_CHKOBJTYPE (timer, OZ_OBJTYPE_TIMER);
  oz_knl_timer_validate ();
  if (timer -> next != timer) oz_knl_timer_remove (timer);
  oz_knl_timer_validate ();
  OZ_KNL_NPPFREE (timer);
}

/************************************************************************/
/*									*/
/*  This routine is called at smplock_tm level by the hardware layer 	*/
/*  when the time is up							*/
/*									*/
/************************************************************************/

void oz_knl_timer_timeisup (void)

{
  intserv_entered ++;

  oz_knl_timer_validate ();

  /* Call the 'timer_call' routine at low ipl to process any completed requests */

  if (timer_lowipl != NULL) {
    oz_knl_lowipl_call (timer_lowipl, timer_call, NULL);
    timer_lowipl = NULL;
  }

  intserv_exited ++;
}

/* This routine is called at softint when there are completed entries to be processed. */
/* It calls the completion routine for each at softint level.                          */

static void timer_call (void *dummy, OZ_Lowipl *lowipl)

{
  uLong tm;
  OZ_Datebin now;
  OZ_Timer *timer;
  void (*entry) (void *param, OZ_Timer *timer);
  void *param;

  OZ_HW_ATOMIC_INCBY1_LONG (lowipl_entered);
  entry = NULL;							/* clear these for debugging (so the crash below will show NULL) */
  param = NULL;
  while (1) {
    tm = oz_hw_smplock_wait (oz_hw_smplock_tm);			/* acquire lock */
    if (tm != OZ_SMPLOCK_SOFTINT) oz_crash ("oz_dev_timer timer_call: smplevel %u (sb SOFTINT), last entry,param = %p,%p", tm, entry, param);
    oz_knl_timer_validate ();
    timer = alltimers;						/* see if there's anything in the queue */
    if (timer == NULL) break;
    now = oz_hw_tod_getnow ();					/* get current date/time */
    if (OZ_HW_DATEBIN_CMP (timer -> datebin, now) > 0) break;	/* see if there are any completed entries */
    alltimers = timer -> next;					/* unlink top completed timer entry */
    timer -> next = timer;					/* remember it is not in queue now */
    entry = timer -> entry;					/* save in case it gets freed the instant we release the smplock */
    param = timer -> param;
    if (lowipl != NULL) {					/* see if first time through the loop */
      if (alltimers == NULL) timer_lowipl = lowipl;		/* if there are no more timers, re-enable lowipl routine */
      else if (OZ_HW_DATEBIN_CMP (alltimers -> datebin, now) <= 0) { /* see if next timer has expired, too */
        oz_knl_lowipl_call (lowipl, timer_call, NULL);		/* if so, arm to call it (maybe on another CPU) */
								/* (also, if our 'entry' routine waits, this timer will get called */
      } else {
        timer_lowipl = lowipl;					/* not expired yet, reset the lowipl routine */
        oz_hw_timer_setevent (alltimers -> datebin);		/* set next event date/time */
      }
      lowipl = NULL;						/* only do this once as we have now disposed of lowipl */
    }
    oz_hw_smplock_clr (oz_hw_smplock_tm, tm);			/* release lock */
    (*entry) (param, timer);					/* call the completion routine */
								/* we have to be fancy above to set up all the timer stuff again before calling completion routine */
								/* ... just in case completion routine does something like start a timer of its own and wait for it */
  }

  if (lowipl != NULL) {
    timer_lowipl = lowipl;					/* make sure we do something with lowipl */
    if (alltimers != NULL) oz_hw_timer_setevent (alltimers -> datebin); /* and timer interrupt is armed */
  }
  oz_hw_smplock_clr (oz_hw_smplock_tm, tm);			/* release lock */
  OZ_HW_ATOMIC_INCBY1_LONG (lowipl_exited);
}

void oz_knl_timer_validate (void)

{
  uLong tm;
  OZ_Datebin last;
  OZ_Timer *timer;

  memset (&last, 0, sizeof last);
  tm = oz_hw_smplock_wait (oz_hw_smplock_tm);
  for (timer = alltimers; timer != NULL; timer = timer -> next) {
    if (!OZ_HW_WRITABLE (sizeof *timer, timer, OZ_PROCMODE_KNL)) {
      oz_knl_printk ("oz_knl_timer_validate: bad timer pointer %p (%p)", timer, oz_hw_getrtnadr (0));
      oz_hw_pte_print (timer);
      oz_crash ("oz_knl_timer_validate: crashing");
    }
    OZ_KNL_CHKOBJTYPE (timer, OZ_OBJTYPE_TIMER);
    if (!OZ_HW_READABLE (1, (void *)(timer -> entry), OZ_PROCMODE_KNL)) oz_crash ("oz_knl_timer_validate: timer %p bad entrypoint %p", timer, timer -> entry);
    if (timer -> param != NULL) {
      if (!OZ_HW_READABLE (1, timer -> param, OZ_PROCMODE_KNL)) oz_crash ("oz_knl_timer_validate: timer %p bad parameter %p", timer, timer -> param);
    }
    if (OZ_HW_DATEBIN_CMP (last, timer -> datebin) > 0) {
      oz_crash ("oz_knl_timer_validate: last %##t after next %##t", last, timer -> datebin);
    }
    last = timer -> datebin;
  }
  oz_hw_smplock_clr (oz_hw_smplock_tm, tm);
}

void oz_knl_timer_debug (void)

{
  uLong tm;
  OZ_Timer *timer;

  tm = oz_hw_smplock_wait (oz_hw_smplock_tm);
  oz_knl_printk ("oz_knl_timer_debug: timer_lowipl %p, intserv_entered %d, intserv_exited %d, lowipl_entered %d, lowipl_exited %d\n", 
                 timer_lowipl, intserv_entered, intserv_exited, lowipl_entered, lowipl_exited);
  for (timer = alltimers; timer != NULL; timer = timer -> next) {
    oz_knl_printk ("oz_knl_timer_debug: %p: %##t\n", timer, timer -> datebin);
  }
  oz_hw_smplock_clr (oz_hw_smplock_tm, tm);
}