/*
 *  Copyright (c) by Jaroslav Kysela (Perex soft)
 *  Memory allocation routines.
 *
 *
 *   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 of the License, 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

#include "driver.h"
#include "info.h"

/*
 *  page allocation
 */

snd_mutex_define_static( memory );
static long snd_pages;

static snd_info_entry_t *snd_memory_info_entry;
#ifdef SNDCFG_DEBUG
static snd_info_entry_t *snd_memory_info_entry_debug;
#endif

#ifdef SNDCFG_DEBUG
static void snd_memory_debug1( void );
#endif

char *snd_malloc_pages( unsigned long size, int *res_pg, int dma )
{
  int pg;
  char *buf;
  
  for ( pg = 0; PAGE_SIZE * ( 1 << pg ) < size; pg++ );
#if LinuxVersionCode( 2, 1, 85 ) <= LINUX_VERSION_CODE
  buf = (char *)__get_free_pages( GFP_KERNEL | (dma ? GFP_DMA : 0), pg );
#else
  buf = (char *)__get_free_pages( GFP_KERNEL, pg, dma ? MAX_DMA_ADDRESS : 0 );
#endif
#if 0 
  snd_printk( "pg = %i, buf = 0x%x\n", pg, (int)buf );
#endif
  if ( buf )
    {
      int page, last_page;
      
      page = MAP_NR( buf );
      last_page = page + ( 1 << pg );
      while ( page < last_page )
        set_bit( PG_reserved, &mem_map[ page++ ].flags );
      snd_pages += 1 << pg;
    }
  *res_pg = pg;
  return buf;
}

void snd_free_pages( char *ptr, unsigned long size )
{
  int pg, page, last_page;

  for ( pg = 0; PAGE_SIZE * ( 1 << pg ) < size; pg++ );
  page = MAP_NR( ptr );
  last_page = page + ( 1 << pg );
  while ( page < last_page )
    clear_bit( PG_reserved, &mem_map[ page++ ].flags );
  free_pages( (unsigned long)ptr, pg );
  snd_pages -= 1 << pg; 
}

/*
 *  ok.. new allocation routines..
 */

#define BLOCKS_IN_PAGE		(PAGE_SIZE / 32)
#define HEADER_SIZE		sizeof( struct alloc_header )
#define FREE_SPACE		(PAGE_SIZE - HEADER_SIZE)
#define ALIGN_VALUE( x )	( ( (x) + ( sizeof( void * ) - 1 ) ) & ~( sizeof( void * ) - 1 ) )

struct alloc_header {
  struct alloc_header *next;			/* next page */
  unsigned short sizes[ BLOCKS_IN_PAGE ];	/* size of block */
};

static struct alloc_header *snd_alloc_first_page;

#ifdef SNDCFG_DEBUG_MEMORY
static void *sndmem_alloc_ptr[ 1024 ];
static unsigned long sndmem_alloc_size[ 1024 ];
#endif

void snd_malloc_init( void )
{
  snd_pages = 0;
  snd_alloc_first_page = NULL;
#ifdef SNDCFG_DEBUG_MEMORY
  memset( sndmem_alloc_ptr, 0, sizeof( sndmem_alloc_ptr ) );
#endif
}

void snd_malloc_done( void )
{
  struct alloc_header *hdr, *next;

  if ( ( hdr = snd_alloc_first_page ) != NULL )
    {
#ifdef SNDCFG_DEBUG
      snd_printk( "malloc done - trouble\n" );
      snd_memory_debug1();
#endif
      while ( hdr )
        {
          next = hdr -> next;
          snd_free_pages( (char *)hdr, PAGE_SIZE );
          hdr = next;
        }
    }    
#ifdef SNDCFG_DEBUG
  if ( snd_pages > 0 )
    snd_printk( "malloc done - snd_pages = %li\n", snd_pages );
#endif
}

void *snd_malloc( unsigned long size )
{
  int pg;

  if ( !size ) return NULL;
#if 0
  snd_printk( "mallocccccccccccccccc %li\n", size );
#endif
  if ( size > FREE_SPACE )
    {
      void *res;
      
      res = snd_malloc_pages( size, &pg, 0 );
#ifdef SNDCFG_DEBUG_MEMORY
      {
        int idx;

        for ( idx = 0; idx < 1024; idx++ )
          if ( !sndmem_alloc_ptr[ idx ] )
            {
              sndmem_alloc_ptr[ idx ] = res;
              sndmem_alloc_size[ idx ] = size;
              break;
            }
      }
#endif
      return res;
    }
   else
    {
      struct alloc_header *hdr;
      struct alloc_header *phdr = NULL;

      snd_mutex_down_static( memory );      
      hdr = snd_alloc_first_page;
      size = ALIGN_VALUE( size );
      while ( hdr )
        {
          int idx;
          unsigned short used;
          
          for ( idx = 0, used = 0; idx < BLOCKS_IN_PAGE; idx++ )
            {
              unsigned short bused;
              unsigned short bsize;
              
              bsize = hdr -> sizes[ idx ];
              bused = bsize & 0x8000;
              bsize &= 0x7fff;
              if ( ( !bsize && ( used + HEADER_SIZE + size <= PAGE_SIZE ) ) || 
                   ( !bused && bsize >= size ) )  /* free & passed */
                {
                  if ( bsize > size )
                    {
                      int i;
                      if ( hdr -> sizes[ BLOCKS_IN_PAGE - 1 ] & 0x8000 ) continue;	/* next block */
                      for ( i = BLOCKS_IN_PAGE - 1; i > idx + 1; i-- )
                        hdr -> sizes[ i ] = hdr -> sizes[ i - 1 ];
                      hdr -> sizes[ idx + 1 ] = bsize - size;
                    }
                  hdr -> sizes[ idx ] = 0x8000 | size;
                  snd_mutex_up_static( memory );
                  return (void *)((char *)hdr + HEADER_SIZE + used);
                }
              used += bsize;
            }
          hdr = ( phdr = hdr ) -> next;
        }
      hdr = (struct alloc_header *)snd_malloc_pages( PAGE_SIZE, &pg, 0 );
      if ( !hdr ) {
        snd_mutex_up_static( memory );
        return NULL;
      }
      memset( hdr, 0, HEADER_SIZE );
      if ( !phdr )
        snd_alloc_first_page = hdr; 
       else
        phdr -> next = hdr;
      hdr -> sizes[ 0 ] = 0x8000 | size;
      snd_mutex_up_static( memory );
      return (void *)((char *)hdr + HEADER_SIZE);
    }
}

void snd_free( void *obj, unsigned long size )
{
  if ( !size ) return;
  if ( !obj )
    {
#ifdef SNDCFG_DEBUG_MEMORY
      snd_printk( "snd_free - NULL?\n" ); 
#endif
      return;
    }
#if 0
  snd_printk( "freeeeeeeeeeee 0x%x, %li\n", obj, size );
#endif
  if ( size > FREE_SPACE )
    {
      snd_free_pages( obj, size );
#ifdef SNDCFG_DEBUG_MEMORY
      {
        int idx;
     
        for ( idx = 0; idx < 1024; idx++ )
          if ( sndmem_alloc_ptr[ idx ] == obj )
            {
              sndmem_alloc_ptr[ idx ] = NULL;
              return;
            }
        snd_printk( "snd_free (1) - free of unallocated block at 0x%lx size %li\n", (long)obj, size );
      }
#endif
    }
   else
    {
      struct alloc_header *hdr;
      struct alloc_header *phdr = NULL;	/* previous header */

      snd_mutex_down_static( memory );
      hdr = snd_alloc_first_page;
      while ( hdr )
        {
          int idx;
          unsigned short used;
          
          for ( idx = 0, used = 0; hdr -> sizes[ idx ] != 0 && idx < BLOCKS_IN_PAGE; idx++ )
            {
              if ( (char *)hdr + HEADER_SIZE + used == obj )
                {
                  hdr -> sizes[ idx ] &= ~0x8000;
                  if ( idx + 1 < BLOCKS_IN_PAGE && hdr -> sizes[ idx + 1 ] == 0 )
                    hdr -> sizes[ idx ] = 0;
                  __again:
                  for ( idx = 0, used = 0; hdr -> sizes[ idx ] != 0 && idx < BLOCKS_IN_PAGE; idx++ )
                    {
                      if ( idx + 1 < BLOCKS_IN_PAGE &&
                           !( hdr -> sizes[ idx ] & 0x8000 ) &&		/* join blocks */
                           !( hdr -> sizes[ idx + 1 ] & 0x8000 ) &&
                           hdr -> sizes[ idx + 1 ] != 0 )
                        {
                          int i;
                        
                          hdr -> sizes[ idx ] += hdr -> sizes[ idx + 1 ];
                          for ( i = idx + 1; hdr -> sizes[ i + 1 ] != 0 && i + 1 < BLOCKS_IN_PAGE; i++ )
                            hdr -> sizes[ i ] = hdr -> sizes[ i + 1 ];
			  hdr -> sizes[ i ] = 0;
			  goto __again;
                        }
                      if ( hdr -> sizes[ idx ] & 0x8000 ) used++;
                    }
                  if ( idx > 0 && !(hdr -> sizes[ idx - 1 ] & 0x8000) )
                    hdr -> sizes[ idx - 1 ] = 0;
                  if ( !used )
                    {
                      if ( phdr )
                        phdr -> next = hdr -> next;
                       else
                        snd_alloc_first_page = hdr -> next;
                      snd_free_pages( (char *)hdr, PAGE_SIZE );
                    }
                  snd_mutex_up_static( memory );
                  return;		/* ok.. successfull */
                }
              used += hdr -> sizes[ idx ] & 0x7fff;
            }
          phdr = hdr;
          hdr = hdr -> next;
        }
#ifdef SNDCFG_DEBUG_MEMORY
      snd_printk( "snd_free (2) - free of unallocated block at 0x%lx size %li\n", (long)obj, size );
#endif
      snd_mutex_up_static( memory ); 
    }
}

char *snd_malloc_strdup( char *string )
{
  int len;
  char *res;

  if ( !string ) return NULL;
  len = strlen( string ) + 1;
  if ( ( res = snd_malloc( len ) ) == NULL ) return NULL;
  memcpy( res, string, len );
  return res;
}

void snd_free_str( char *string )
{
  if ( string )
    snd_free( string, strlen( string ) + 1 );
}

static void snd_memory_info_read( snd_info_buffer_t *buffer, void *private_data )
{
  snd_iprintf( buffer, "Driver using %i page%s (%li bytes) of kernel memory for data.\n",
          			snd_pages,
          			snd_pages > 1 ? "s" : "",
		          	snd_pages * PAGE_SIZE );
}

#ifdef SNDCFG_DEBUG_MEMORY
static void snd_memory_info_debug_read( snd_info_buffer_t *buffer, void *private_data )
{
  int i, used, free, total_blocks, total_used;
  struct alloc_header *hdr;

  snd_mutex_down_static( memory );
  snd_iprintf( buffer, "Memory allocation table:\n" );
  for ( i = used = 0; i < 1024; i++ )
    if ( sndmem_alloc_ptr[ i ] )
      {
        used++;
        snd_iprintf( buffer, "  %04i: 0x%08lx [%08i] = %8li\n",
      					i,
        				sndmem_alloc_ptr[ i ],
        				MAP_NR( sndmem_alloc_ptr[ i ] ),
        				sndmem_alloc_size[ i ] );
      }
  for ( hdr = snd_alloc_first_page; hdr; hdr = hdr -> next, i++ )
    snd_iprintf( buffer, "  %04i: 0x%08lx [%08i] = %8li (internal)\n", i, (long)hdr, MAP_NR( hdr ), PAGE_SIZE );
  if ( !used && !snd_alloc_first_page )
    snd_iprintf( buffer, "  -none-\n" );
  snd_iprintf( buffer, "\nMemory allocation table (internal):\n" );
  hdr = snd_alloc_first_page;
  if ( !hdr )
    snd_iprintf( buffer, "  -none-\n" );
   else
    {
      total_blocks = total_used = 0;
      while ( hdr )
        {
          snd_iprintf( buffer, "  page at 0x%lx:\n", (long)hdr );
          for ( i = used = free = 0; i < BLOCKS_IN_PAGE; i++ )
            {
              unsigned short bsize, bused;
              
              bsize = hdr -> sizes[ i ];
              if ( !bsize ) continue;
              bused = bsize & 0x8000;
              bsize &= 0x7fff;
              total_blocks++;
              snd_iprintf( buffer, "    %04i: 0x%08lx = %4li (%s)\n", i, (long)((char *)hdr + HEADER_SIZE + used), bsize, bused ? "allocated" : "free" );
              if ( !bused ) free += bsize; else total_used += bsize;
              used += bsize;
            }
          snd_iprintf( buffer, "    sum : size = %li, used = %li, free = %li, unused = %li\n", PAGE_SIZE - HEADER_SIZE, used - free, free, PAGE_SIZE - HEADER_SIZE - used );
          hdr = hdr -> next;
        }
      snd_iprintf( buffer, "    total: blocks = %i, used = %i, recomended block allocation = %i\n",
      			total_blocks, total_used,
      			PAGE_SIZE / ( total_used / total_blocks ) );
    }
  snd_mutex_up_static( memory );
}
#endif

int snd_memory_info_init( void )
{
  snd_info_entry_t *entry;
#ifdef SNDCFG_DEBUG_MEMORY
  snd_info_entry_t *entry1;
#endif

  entry = snd_info_create_entry( NULL, "meminfo" );
  if ( !entry ) return -ENOMEM;
  entry -> t.text.read_size = 256;
  entry -> t.text.read = snd_memory_info_read;
  if ( snd_info_register( entry ) < 0 ) {
    snd_info_free_entry( entry );
    return -ENOMEM;
  }
#ifdef SNDCFG_DEBUG_MEMORY
  entry1 = snd_info_create_entry( NULL, "memdebug" );
  if ( !entry1 ) return -ENOMEM;
  entry1 -> t.text.read_size = 512 * 1024;
  entry1 -> t.text.read = snd_memory_info_debug_read;
  if ( snd_info_register( entry1 ) < 0 ) {
    snd_info_unregister( entry );
    snd_info_free_entry( entry1 );
    return -ENOMEM;
  }
#endif
  snd_memory_info_entry = entry;
#ifdef SNDCFG_DEBUG_MEMORY
  snd_memory_info_entry_debug = entry1;
#endif
  return 0;
}

int snd_memory_info_done( void )
{
  if ( snd_memory_info_entry )
    snd_info_unregister( snd_memory_info_entry );
#ifdef SNDCFG_DEBUG_MEMORY
  if ( snd_memory_info_entry_debug )
    snd_info_unregister( snd_memory_info_entry_debug );
#endif
  return 0;
}

#ifdef SNDCFG_DEBUG
static void snd_memory_debug1( void )
{
  int i, used, free, total_blocks, total_used;
  struct alloc_header *hdr;

  snd_mutex_down_static( memory );
  printk( "Memory allocation table:\n" );
  for ( i = used = 0; i < 1024; i++ )
    if ( sndmem_alloc_ptr[ i ] )
      {
        used++;
        printk( "  %04i: 0x%08lx [%08li] = %8li\n",
      					i,
        				(long)sndmem_alloc_ptr[ i ],
        				(long)MAP_NR( sndmem_alloc_ptr[ i ] ),
        				sndmem_alloc_size[ i ] );
      }
  for ( hdr = snd_alloc_first_page; hdr; hdr = hdr -> next, i++ )
    printk( "  %04i: 0x%08lx [%08li] = %8li (internal)\n", i, (long)hdr, (long)MAP_NR( hdr ), (long)PAGE_SIZE );
  if ( !used && !snd_alloc_first_page )
    printk( "  -none-\n" );
  printk( "Memory allocation table (internal):\n" );
  hdr = snd_alloc_first_page;
  if ( !hdr )
    printk( "  -none-\n" );
   else
    {
      total_blocks = total_used = 0;
      while ( hdr )
        {
          printk( "  page at 0x%lx:\n", (long)hdr );
          for ( i = used = free = 0; i < BLOCKS_IN_PAGE; i++ )
            {
              unsigned short bsize, bused;
              
              bsize = hdr -> sizes[ i ];
              if ( !bsize ) continue;
              bused = bsize & 0x8000;
              bsize &= 0x7fff;
              total_blocks++;
              printk( "    %04i: 0x%08lx = %4i (%s)\n", i, (long)((char *)hdr + HEADER_SIZE + used), bsize, bused ? "allocated" : "free" );
              if ( !bused ) free += bsize; else total_used += bsize;
              used += bsize;
            }
          printk( "    sum : size = %li, used = %i, free = %li, unused = %li\n", PAGE_SIZE - HEADER_SIZE, used - free, (long)free, PAGE_SIZE - HEADER_SIZE - used );
          hdr = hdr -> next;
        }
      printk( "    total: blocks = %i, used = %i, recomended block allocation = %li\n",
      			total_blocks, total_used,
      			(long)(PAGE_SIZE / ( total_used / total_blocks )) );
    }
  snd_mutex_up_static( memory );
}
#endif

/*
 *  DMA allocation routines
 */

int snd_dma_malloc( snd_card_t *card, int dmanum, char *owner, int soft )
{
  struct snd_stru_dma *dma;
  unsigned char *buf;
  long size;
  int pg = 0;

  if ( dmanum < 0 || dmanum >= SND_DMAS ) return -EINVAL;
  dma = card -> dmas[ dmanum ];
  if ( dma -> type == SND_DMA_TYPE_HARDWARE ) return -EINVAL;
  snd_mutex_down( dma, mutex );
  if ( !soft ) {
    if ( dma -> owner ) {
      if ( dma -> mmaped ) {
        dma -> mmap_free = 0;
        snd_mutex_up( dma, mutex );
        return 0;
      } else {
        snd_mutex_up( dma, mutex );
        return -EBUSY;
      }
    }
    dma -> mmap_free = 0;
  } else {
    if ( dma -> soft_owner ) {
      snd_mutex_up( dma, mutex );
      return -EBUSY;
    }
  }  
  if ( dma -> buf ) {			/* allocated */
    if ( !soft ) {
      snd_mutex_down( dma, lock );	/* this waits until DMA isn't free and locks it again */
      dma -> owner = owner;
    } else {
      dma -> soft_owner = owner;
    }
    snd_mutex_up( dma, mutex );
    return 0;				/* ok... dma is allocated */
  }
  buf = NULL;
  size = dma -> rsize;
  while ( !buf && size >= PAGE_SIZE ) {
    buf = snd_malloc_pages( size, &pg, dma -> type == SND_DMA_TYPE_ISA ? 1 : 0 );
    if ( !buf ) size >>= 1;
  }
  if ( buf ) {
    dma -> size = ( 1 << pg ) * PAGE_SIZE;
    dma -> buf = buf;
    if ( !soft ) {
      snd_mutex_down( dma, lock );	/* lock this dma channel */
      dma -> owner = owner;
    } else {
      dma -> soft_owner = owner;
    }
  }
  snd_mutex_up( dma, mutex );
  return buf == NULL ? -ENOMEM : 0;
}

void snd_dma_free( snd_card_t *card, int dmanum, int soft )
{
  struct snd_stru_dma *dma;

  if ( dmanum < 0 || dmanum >= SND_DMAS ) return;
  dma = card -> dmas[ dmanum ];
  if ( dma -> type == SND_DMA_TYPE_HARDWARE ) return;
#if 0
  snd_printk( "snd_dma_free: dmanum = %i, soft = %i, mmaped = %i\n", dmanum, soft, dma -> mmaped );
#endif
  snd_mutex_down( dma, mutex );
  if ( !soft || soft == -1 ) {
    if ( dma -> mmaped ) {		/* wait */
      dma -> mmap_free = 1;
      snd_mutex_up( dma, mutex );
      return;
    }
    dma -> mmap_free = 0;
    dma -> owner = NULL;
    snd_mutex_up( dma, lock );
  } else {
    dma -> soft_owner = NULL;
  }
  if ( dma -> buf && !dma -> owner && !dma -> soft_owner ) {
    /* free DMA buffer */
    snd_free_pages( dma -> buf, dma -> size );
    dma -> buf = NULL;
    dma -> size = 0;
  }
  snd_mutex_up( dma, mutex ); 
}

int snd_dma_soft_grab( snd_card_t *card, int dmanum )
{
  return -EBUSY;
}

void snd_dma_soft_release( snd_card_t *card, int dmanum )
{
}

void snd_dma_notify_vma_close( struct vm_area_struct *area )
{
  int idx, idx1;
  snd_card_t *card;
  struct snd_stru_dma *dma;

  for ( idx = 0; idx < snd_ecards_limit; idx++ ) {
    if ( !(snd_cards_bitmap & (1 << idx)) ) continue;
    card = snd_cards[ idx ];
    if ( !card ) continue;
    for ( idx1 = 0; idx1 < SND_DMAS; idx1++ ) {
      if ( !(card -> dmas_map & (1 << idx1)) ) continue;
      dma = card -> dmas[ idx1 ];
      if ( !dma ) continue;
      if ( dma -> mmaped && dma -> vma == area ) {	/* yep! */
        snd_mutex_down( dma, mutex );
        dma -> vma = NULL;
        dma -> mmaped = 0;
        if ( dma -> mmap_free ) {
          snd_mutex_up( dma, mutex );
          snd_dma_free( card, idx1, -1 );
        } else {
          snd_mutex_up( dma, mutex );
        }
      }
    }
  }
}
