/*
 * Purpose: Operating system abstraction functions for FreeBSD
 */
/*
 * This file is part of Open Sound System
 *
 * Copyright (C) 4Front Technologies 1996-2007.
 *
 * This software is released under CDDL 1.0 source license for operating systems
 * that have their full source code available under the BSD or CDDL licenses.
 *
 * See the COPYING file included in the main directory of this source
 * distribution. Please contact sales@opensound.com for further info.
 */

/*
 * NOTE! This is not the development version of this file. Please download the
 * sources of the latest Open Sound System version from 
 * http://developer.opensound.com/sources if you plan to make any changes
 * to the software.
 */

#include "oss_config.h"
#include "midi_core.h"
#include <oss_pci.h>
#include <sys/conf.h>
#include <sys/proc.h>
#include <sys/sx.h>
#include <sys/mman.h>
#include <fs/devfs/devfs.h>
#include <sys/poll.h>

/* Function prototypes */
static d_open_t oss_open;
static d_close_t oss_close;
static d_read_t oss_read;
static d_write_t oss_write;
static d_ioctl_t oss_ioctl;
static d_poll_t oss_poll;
static d_mmap_t oss_mmap;

/* Character device entry points */

static struct cdevsw oss_cdevsw = {
  .d_version = D_VERSION,
  .d_flags = 0,
  .d_open = oss_open,
  .d_close = oss_close,
  .d_read = oss_read,
  .d_write = oss_write,
  .d_ioctl = oss_ioctl,
  .d_poll = oss_poll,
  .d_mmap = oss_mmap,
  .d_name = "oss"
};

static int oss_major = 30;
oss_device_t *core_osdev = NULL;
static int open_devices = 0;
static int refcount = 0;

#define MAX_CARDS	16
int oss_num_cards = 0;
static oss_device_t *cards[MAX_CARDS];
static int oss_expired = 0;

int
__oss_alloc_dmabuf (int dev, dmap_p dmap, unsigned int alloc_flags,
		    oss_uint64_t maxaddr, int direction)
{
  void *tmpbuf;
  oss_native_word phaddr;
  int size = 64 * 1024;
  extern int dma_buffsize;

  if (dma_buffsize > 16 && dma_buffsize <= 128)
    size = dma_buffsize * 1024;

  if (dmap->dmabuf != NULL)
    return 0;

/*
 * Some applications and virtual drivers need shorter buffer.
 */
  if (dmap->flags & DMAP_SMALLBUF)
    {
      size = SMALL_DMABUF_SIZE;
    }
  else if (dmap->flags & DMAP_MEDIUMBUF)
    {
      size = MEDIUM_DMABUF_SIZE;
    }

  if ((alloc_flags & DMABUF_SIZE_16BITS) && size > 32 * 1024)
    size = 32 * 1024;

  tmpbuf = CONTIG_MALLOC (dmap->osdev, size, maxaddr, &phaddr);
  if (tmpbuf == NULL)
    return -ENOMEM;
  dmap->dmabuf = tmpbuf;
  dmap->buffsize = size;
  dmap->dmabuf_phys = phaddr;

  return 0;
}

void
oss_free_dmabuf (int dev, dmap_p dmap)
{
  if (dmap->dmabuf == NULL)
    return;

  CONTIG_FREE (dmap->osdev, dmap->dmabuf, dmap->buffsize);
  dmap->dmabuf = NULL;
  dmap->buffsize = 0;
  dmap->dmabuf_phys = 0;
}

oss_wait_queue_t *
oss_create_wait_queue (oss_device_t * osdev, const char *name)
{
  oss_wait_queue_t *wq;

  if ((wq = PMALLOC (NULL, sizeof (*wq))) == NULL)
    return NULL;

  MUTEX_INIT (osdev, wq->mutex, MH_TOP);

  return wq;
}

void
oss_reset_wait_queue (oss_wait_queue_t * wq)
{
  wq->flags = 0;
}

void
oss_remove_wait_queue (oss_wait_queue_t * wq)
{
  MUTEX_CLEANUP (wq->mutex);
}

int
oss_sleep (oss_wait_queue_t * wq, oss_mutex_t * mutex, int ticks,
	   oss_native_word * flags, unsigned int *status)
{
  int flag;
  *status = 0;

  if (wq == NULL)
    return 0;

  wq->flags = 0;
  flag = msleep (wq, *mutex, PRIBIO | PCATCH, "oss", ticks);

  if (flag == EWOULDBLOCK)	/* Timeout */
    {
      return 0;
    }

  if (flag != 0)
    {
      *status |= WK_SIGNAL;
    }

  return 1;
}

int
oss_register_poll (oss_wait_queue_t * wq, oss_mutex_t * mutex,
		   oss_native_word * flags, oss_poll_event_t * ev)
{
  selrecord (ev->p, &wq->poll_info);
  return 0;
}

void
oss_wakeup (oss_wait_queue_t * wq, oss_mutex_t * mutex,
	    oss_native_word * flags, short events)
{
  wakeup (wq);
  selwakeup (&wq->poll_info);
}

void
oss_register_module (char *name)
{
  refcount++;
}

void
oss_unregister_module (char *name)
{
  refcount--;
}

void
oss_reserve_device (oss_device_t * osdev)
{
  osdev->refcount++;
}

void
oss_unreserve_device (oss_device_t * osdev)
{
  osdev->refcount--;
  if (osdev->refcount < 0)
    osdev->refcount = 0;
}

int
oss_register_device (oss_device_t * osdev, const char *name)
{
  if ((osdev->name = PMALLOC (NULL, strlen (name) + 1)) == NULL)
    {
      cmn_err (CE_WARN, "Cannot allocate memory for device name\n");
      osdev->name = "Unknown device";
    }
  strcpy (osdev->name, name);
  if (osdev->dip != NULL)
    device_set_desc (osdev->dip, name);
  return -ENXIO;
}

void
oss_unregister_device (oss_device_t * osdev)
{
  // NOP
}

int
oss_find_minor (int dev_class, int instance)
{
  int i;

  for (i = 0; i < oss_num_cdevs; i++)
    if (oss_cdevs[i]->d != NULL && oss_cdevs[i]->dev_class == dev_class
	&& oss_cdevs[i]->instance == instance)
      return i;

  return -1;
}

void *
oss_find_minor_info (int dev_class, int instance)
{
  int i;

  for (i = 0; i < oss_num_cdevs; i++)
    {
      if (oss_cdevs[i]->d != NULL && oss_cdevs[i]->dev_class == dev_class
	  && oss_cdevs[i]->instance == instance)
	return oss_cdevs[i]->info;
    }

  return NULL;
}

int
oss_disable_device (oss_device_t * osdev)
{
  int i;

  if (osdev->refcount > 0)
    return -EBUSY;

  if (open_devices > 0)
    return -EBUSY;

  for (i = 0; i < num_mixers; i++)
    if (mixer_devs[i]->osdev == osdev)
      {
	mixer_devs[i]->unloaded = 1;
      }

  for (i = 0; i < num_mididevs; i++)
    {
      if (midi_devs[i]->osdev == osdev)
	{
	  midi_devs[i]->unloaded = 1;
	  if (midi_devs[i]->converter != NULL)
	    midi_devs[i]->converter->enabled = 0;
	}
    }

  for (i = 0; i < num_audio_engines; i++)
    if (audio_engines[i]->osdev == osdev)
      {
	audio_uninit_device (i);
      }
  return 0;
}

int
oss_get_cardinfo (int cardnum, oss_card_info * ci)
{
/*
 * Print information about a 'card' in a format suitable for /dev/sndstat
 */

  if (cardnum < 0 || cardnum >= oss_num_cards)
    return -ENXIO;

  if (cards[cardnum]->name != NULL)
    strncpy (ci->longname, cards[cardnum]->name, 128);
  ci->shortname[127] = 0;

  if (cards[cardnum]->nick != NULL)
    strncpy (ci->shortname, cards[cardnum]->nick, 16);
  ci->shortname[15] = 0;

  return 0;
}

void
oss_install_chrdev (oss_device_t * osdev, char *name, int dev_class,
		    int instance, oss_cdev_drv_t * drv, int flags)
{
  struct cdev *bsd_cdev;
  oss_cdev_t *cdev = NULL;
  int i, num;

  if (dev_class != OSS_DEV_STATUS)
    if (oss_expired && instance > 0)
      return;
/*
 * Find if this dev_class&instance already exists (after previous module
 * detach).
 */

  for (num = 0; num < oss_num_cdevs; num++)
    if (oss_cdevs[num]->d == NULL)	/* Unloaded driver */
      if (oss_cdevs[num]->dev_class == dev_class
	  && oss_cdevs[num]->instance == instance)
	{
	  cdev = oss_cdevs[num];
	  break;
	}

  if (cdev == NULL)
    {
      if (oss_num_cdevs >= OSS_MAX_CDEVS)
	{
	  cmn_err (CE_WARN, "Out of minor numbers.\n");
	  return;
	}
      num = oss_num_cdevs++;

      if ((cdev = PMALLOC (NULL, sizeof (*cdev))) == NULL)
	{
	  cmn_err (CE_WARN, "Cannot allocate character device desc.\n");
	  return;
	}
    }

  memset (cdev, 0, sizeof (*cdev));
  cdev->dev_class = dev_class;
  cdev->instance = instance;
  cdev->d = drv;
  cdev->osdev = osdev;
  if (name != NULL)
    strncpy (cdev->name, name, sizeof (cdev->name) - 1);
  else
    strcpy (cdev->name, "NONE");
  cdev->name[sizeof (cdev->name) - 1] = 0;
  oss_cdevs[num] = cdev;

  if (!(flags & CHDEV_VIRTUAL))
    {
      bsd_cdev =
	make_dev (&oss_cdevsw, num, UID_ROOT, GID_WHEEL, 0666, name, 0);
      cdev->info = bsd_cdev;
    }
}

#define MAX_MEMBLOCKS	4096
static void *memblocks[MAX_MEMBLOCKS];
static int nmemblocks = 0;

void *
oss_pmalloc (size_t sz)
{
  void *tmp;

  tmp = KERNEL_MALLOC (sz);

  if (nmemblocks < MAX_MEMBLOCKS)
    memblocks[nmemblocks++] = tmp;

  return tmp;
}

int
oss_uiomove (void *address, size_t nbytes, enum uio_rw rwflag, uio_t * uio_p)
{
  int err;

#undef uiomove
  err = uiomove (address, nbytes, uio_p);

  return err;
}

#undef timeout
#undef untimeout

typedef struct tmout_desc
{
  volatile int active;
  int timestamp;
  void (*func) (void *);
  void *arg;

  struct callout_handle timer;
} tmout_desc_t;

static volatile int next_id = 0;
#define MAX_TMOUTS 128

tmout_desc_t tmouts[MAX_TMOUTS] = { {0} };

int timeout_random = 0x12123400;

void
oss_timer_callback (void *arg)
{
  int ix;
  tmout_desc_t *tmout = arg;

  timeout_random++;

  if (!tmout->active)
    return;

  arg = tmout->arg;
  tmout->active = 0;
  tmout->timestamp = 0;

  tmout->func (arg);
}

timeout_id_t
oss_timeout (void (*func) (void *), void *arg, unsigned long long ticks)
{
  tmout_desc_t *tmout = NULL;
  int id, n;

  timeout_random++;

  n = 0;
  id = -1;

  while (id == -1 && n < MAX_TMOUTS)
    {
      if (!tmouts[next_id].active)
	{
	  tmouts[next_id].active = 1;
	  id = next_id++;
	  tmout = &tmouts[id];
	  break;
	}

      next_id = (next_id + 1) % MAX_TMOUTS;
    }

  if (id == -1)			/* No timer slots available */
    {
      cmn_err (CE_CONT, "Timeout table full\n");
      return 0;
    }

  tmout->func = func;
  tmout->arg = arg;
  tmout->timestamp = id | (timeout_random & ~0xff);

  tmout->timer = timeout (oss_timer_callback, tmout, ticks);

  return id | (timeout_random & ~0xff);
}

void
oss_untimeout (timeout_id_t id)
{
  tmout_desc_t *tmout;
  int ix;

  ix = id & 0xff;
  if (ix < 0 || ix >= MAX_TMOUTS)
    return;

  timeout_random++;
  tmout = &tmouts[ix];

  if (tmout->timestamp != id)	/* Expired timer */
    return;
  if (tmout->active)
    untimeout (oss_timer_callback, tmout, tmout->timer);
  tmout->active = 0;
  tmout->timestamp = 0;
}

unsigned long
oss_get_time (void)
{
  struct timeval timecopy;

  getmicrotime (&timecopy);
  return timecopy.tv_usec / (1000000 / hz) + (u_long) timecopy.tv_sec * hz;
}

caddr_t
oss_map_pci_mem (oss_device_t * osdev, int nr, int paddr, int psize)
{
  void *vaddr = 0;
  u_int32_t poffs;

  poffs = paddr - trunc_page (paddr);
  vaddr = (caddr_t) pmap_mapdev (paddr - poffs, psize + poffs) + poffs;

  return vaddr;
}

void
oss_pci_byteswap (oss_device_t * osdev, int mode)
{
  // NOP
}

static time_t
oss_get_walltime (void)
{
  struct timeval timecopy;

  getmicrotime (&timecopy);
  return timecopy.tv_sec;
}

int
soundcard_attach (void)
{
  oss_device_t *osdev;

  if ((osdev = PMALLOC (NULL, sizeof (*osdev))) == NULL)
    {
      return ENOSPC;
    }

  memset (osdev, 0, sizeof (*osdev));
  core_osdev = osdev;

#ifdef LICENSED_VERSION
  if (!oss_license_handle_time (oss_get_walltime ()))
    {
      cmn_err (CE_WARN, "This version of Open Sound System has expired\n");
      cmn_err (CE_CONT,
	       "Please download the latest version from www.opensound.com\n");
      oss_expired = 1;
    }
#endif

  osdev->major = oss_major;
  oss_register_device (osdev, "OSS core services");

  oss_common_init (osdev);

  return 0;
}

int
soundcard_detach (void)
{
  int i;

  if (refcount > 0 || open_devices > 0)
    return EBUSY;

  oss_unload_drivers ();

  osdev_delete (core_osdev);

  for (i = 0; i < nmemblocks; i++)
    KERNEL_FREE (memblocks[i]);
  nmemblocks = 0;

  return 0;
}

static void
init_fileinfo (struct fileinfo *fi, int flags)
{
  memset (fi, 0, sizeof (*fi));
  if ((flags & FREAD) && (flags & FWRITE))
    fi->mode = OPEN_READWRITE;
  else if (flags & FREAD)
    fi->mode = OPEN_READ;
  else if (flags & FWRITE)
    fi->mode = OPEN_WRITE;
  fi->acc_flags = flags;
  fi->pid = -1;
  fi->cmd = NULL;
}

static int
oss_read (struct cdev *bsd_dev, struct uio *buf, int flags)
{
  struct fileinfo fi;
  int retval;
  int dev = minor (bsd_dev);
  oss_cdev_t *cdev;

  if (dev >= oss_num_cdevs)
    return ENXIO;

  if ((cdev = oss_cdevs[dev]) == NULL || cdev->d == NULL)
    return ENXIO;

  if (cdev->d->read == NULL)
    {
      return ENODEV;
    }

  init_fileinfo (&fi, flags);
  retval = cdev->d->read (cdev->instance, &fi, buf, buf->uio_resid);
  if (retval < 0)
    return -retval;
  return 0;

}

static int
oss_write (struct cdev *bsd_dev, struct uio *buf, int flags)
{
  struct fileinfo fi;
  int retval;
  int dev = minor (bsd_dev);
  oss_cdev_t *cdev;

  if (dev >= oss_num_cdevs)
    return ENXIO;

  if ((cdev = oss_cdevs[dev]) == NULL || cdev->d == NULL)
    return ENXIO;

  if (cdev->d->write == NULL)
    {
      return ENODEV;
    }

  init_fileinfo (&fi, flags);
  retval = cdev->d->write (cdev->instance, &fi, buf, buf->uio_resid);
  if (retval < 0)
    return -retval;
  return 0;
}

static int
oss_open (struct cdev *bsd_dev, int flags, int mode, struct thread *p)
{
  int dev = minor (bsd_dev);
  oss_cdev_t *cdev;
  struct fileinfo fi;
  int tmpdev, retval;

  if (dev >= oss_num_cdevs)
    return ENXIO;

  if ((cdev = oss_cdevs[dev]) == NULL || cdev->d == NULL)
    return ENXIO;

  if (cdev->d->open == NULL)
    {
      return ENODEV;
    }

  init_fileinfo (&fi, flags);
  fi.pid = p->td_proc->p_pid;
  fi.cmd = p->td_proc->p_comm;

  retval =
    cdev->d->open (cdev->instance, cdev->dev_class, &fi, 0, 0, &tmpdev);

  if (retval < 0)
    return -retval;

  open_devices++;
  return 0;
}

static int
oss_close (struct cdev *bsd_dev, int flags, int mode, struct thread *p)
{
  struct fileinfo fi;
  int dev = minor (bsd_dev);
  oss_cdev_t *cdev;

  if (dev >= oss_num_cdevs)
    return ENXIO;

  if ((cdev = oss_cdevs[dev]) == NULL || cdev->d == NULL)
    return ENXIO;

  if (cdev->d->close == NULL)
    {
      return ENODEV;
    }

  init_fileinfo (&fi, flags);
  cdev->d->close (cdev->instance, &fi);
  open_devices--;
  return 0;
}

static int
oss_ioctl (struct cdev *bsd_dev, u_long cmd, caddr_t arg, int mode,
	   struct thread *p)
{
  struct fileinfo fi;
  int retval;
  int dev = minor (bsd_dev);
  oss_cdev_t *cdev;

  if (dev >= oss_num_cdevs)
    return ENXIO;

  if ((cdev = oss_cdevs[dev]) == NULL || cdev->d == NULL)
    return ENXIO;

  if (cdev->d->ioctl == NULL)
    {
      return ENODEV;
    }

  init_fileinfo (&fi, mode);
  retval = cdev->d->ioctl (cdev->instance, &fi, cmd, (ioctl_arg) arg);
  if (retval < 0)
    return -retval;
  return 0;
}

static int
oss_poll (struct cdev *bsd_dev, int events, struct thread *p)
{
  int retval;
  int dev = minor (bsd_dev);
  oss_cdev_t *cdev;
  oss_poll_event_t ev;
  struct fileinfo fi;
  int err;

  if (dev >= oss_num_cdevs)
    return ENXIO;

  if ((cdev = oss_cdevs[dev]) == NULL || cdev->d == NULL)
    return ENXIO;

  if (cdev->d->chpoll == NULL)
    {
      return ENODEV;
    }

  init_fileinfo (&fi, 0);

  ev.events = events;
  ev.revents = 0;
  ev.p = p;
  ev.bsd_dev = bsd_dev;

  err = cdev->d->chpoll (cdev->instance, &fi, &ev);
  if (err < 0)
    {
      return -err;
    }
  return ev.revents;
}

static int
oss_mmap (struct cdev *bsd_dev, vm_offset_t offset, vm_paddr_t * paddr,
	  int nprot)
{
  int retval;
  int dev = minor (bsd_dev);
  oss_cdev_t *cdev;
  oss_poll_event_t ev;
  dmap_p dmap = NULL;
  int err;

  if (dev >= oss_num_cdevs)
    return ENXIO;

  if ((cdev = oss_cdevs[dev]) == NULL || cdev->d == NULL)
    return ENXIO;

  if (nprot & PROT_EXEC)
    return EACCES;

  if (cdev->dev_class != OSS_DEV_DSP)	/* Only /dev/dsp devices can be mmapped */
    {
      cmn_err (CE_NOTE, "mmap() is only possible with DSP devices (%d)\n",
	       cdev->dev_class);
      return EINVAL;
    }

  dev = cdev->instance;

  if (dev < 0 || dev >= num_audio_engines)
    return ENODEV;

  if (nprot & PROT_WRITE)
    dmap = audio_engines[dev]->dmap_out;
  else
    dmap = audio_engines[dev]->dmap_in;

  if (dmap == NULL)
    return EIO;

  if (dmap->dmabuf_phys == 0)
    return EIO;

  *paddr = dmap->dmabuf_phys + offset;

  return 0;
}

oss_device_t *
osdev_create (dev_info_t * dip, int dev_type, int instance, const char *nick,
	      const char *handle)
{
  oss_device_t *osdev = NULL;
  int i, err;
  caddr_t addr;
  off_t region_size;

  if (handle == NULL)
    handle = nick;

  /*
   * Don't accept any more drivers if expired
   */
  if (oss_expired && oss_num_cards > 0)
    return NULL;

  for (i = 0; i < oss_num_cards; i++)
    {
      if (cards[i]->dip == dip)
	{
	  osdev = cards[i];
	  break;
	}
    }

  if (osdev == NULL)
    {
      if (oss_num_cards >= MAX_CARDS)
	{
	  cmn_err (CE_WARN, "Too many OSS devices. At most %d permitted.\n",
		   MAX_CARDS);
	  return NULL;
	}

      if ((osdev = PMALLOC (NULL, sizeof (*osdev))) == NULL)
	{
	  cmn_err (CE_WARN, "osdev_create: Out of memory\n");
	  return NULL;
	}

      osdev->cardnum = oss_num_cards;
      cards[oss_num_cards++] = osdev;
    }

  osdev->dip = dip;
  osdev->osid = dip;
  osdev->instance = instance;
  osdev->dev_type = dev_type;
  osdev->devc = NULL;
  sprintf (osdev->nick, "%s%d", nick, instance);
  strcpy (osdev->modname, nick);

/*
 * Create the device handle
 */
  switch (dev_type)
    {
    case DRV_PCI:
      {
	sprintf (osdev->handle, "OSS-PCI");
      }
      break;

    default:
      sprintf (osdev->handle, "%s%d", handle, instance);
    }

  return osdev;
}

oss_device_t *
osdev_clone (oss_device_t * orig_osdev, int new_instance)
{
  oss_device_t *osdev;

  osdev = PMALLOC (NULL, sizeof (*osdev));
  if (osdev == NULL)
    {
      cmn_err (CE_WARN, "osdev_create: Out of memory\n");
      return NULL;
    }
  memcpy (osdev, orig_osdev, sizeof (*osdev));
  osdev->dev_type = DRV_CLONE;
  osdev->instance = new_instance;
  sprintf (osdev->nick, "%s%d", orig_osdev->modname, new_instance);
  sprintf (osdev->handle, "%s%d", orig_osdev->modname, new_instance);

  return osdev;
}

void
osdev_delete (oss_device_t * osdev)
{
  int i;

  if (osdev == NULL)
    return;

/*
 * Mark all minor nodes for this module as invalid.
 */
  for (i = 0; i < oss_num_cdevs; i++)
    if (oss_cdevs[i]->osdev == osdev)
      {
	if (oss_cdevs[i]->info != NULL)
	  destroy_dev (oss_cdevs[i]->info);
	oss_cdevs[i]->d = NULL;
	oss_cdevs[i]->info = NULL;
	oss_cdevs[i]->osdev = NULL;
	strcpy (oss_cdevs[i]->name, "Removed device");
      }
}

void *
oss_get_osid (oss_device_t * osdev)
{
  return osdev->osid;
}
