/*
 * block - fixed, dynamic, fifo and circular memory blocks
 *
 * Copyright (C) 1999-2007,2021-2023  Landon Curt Noll and Ernest Bowen
 *
 * Primary author:  Landon Curt Noll
 *
 * Calc is open software; you can redistribute it and/or modify it under
 * the terms of the version 2.1 of the GNU Lesser General Public License
 * as published by the Free Software Foundation.
 *
 * Calc 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 Lesser General
 * Public License for more details.
 *
 * A copy of version 2.1 of the GNU Lesser General Public License is
 * distributed with calc under the filename COPYING-LGPL.  You should have
 * received a copy with calc; if not, write to Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Under source code control:   1997/02/27 00:29:40
 * File existed as early as:    1997
 *
 * chongo <was here> /\oo/\     http://www.isthe.com/chongo/
 * Share and enjoy!  :-)        http://www.isthe.com/chongo/tech/comp/calc/
 */


#include <stdio.h>
#include "have_string.h"
#ifdef HAVE_STRING_H
# include <string.h>
#endif
#include "alloc.h"
#include "value.h"
#include "zmath.h"
#include "config.h"
#include "block.h"
#include "nametype.h"
#include "str.h"


#include "errtbl.h"
#include "banned.h"     /* include after system header <> includes */


#define NBLOCKCHUNK 16

STATIC long nblockcount = 0;
STATIC long maxnblockcount = 0;
STATIC STRINGHEAD nblocknames;
STATIC NBLOCK **nblocks;


/* forward declarations */
S_FUNC void blkchk(BLOCK*);


/*
 * blkalloc - allocate a block
 *
 * given:
 *      len     - initial memory length of the block
 *      type    - BLK_TYPE_XYZ
 *      chunk   - allocation chunk size
 *
 * returns:
 *      pointer to a newly allocated BLOCK
 */
BLOCK *
blkalloc(int len, int chunk)
{
        BLOCK *nblk;    /* new block allocated */

        /*
         * firewall
         */
        if (len < 0)
                len = 0;
        if (chunk <= 0)
                chunk = BLK_CHUNKSIZE;

        /*
         * allocate BLOCK
         */
        nblk = (BLOCK *)malloc(sizeof(BLOCK));
        if (nblk == NULL) {
                math_error("cannot allocate block");
                not_reached();
        }

        /*
         * initialize BLOCK
         */
        nblk->blkchunk = chunk;
        nblk->maxsize = ((len+chunk)/chunk)*chunk;
        nblk->data = (USB8*)malloc(nblk->maxsize);
        if (nblk->data == NULL) {
                math_error("cannot allocate block data storage");
                not_reached();
        }
        memset(nblk->data, 0, nblk->maxsize);
        nblk->datalen = len;

        /*
         * return BLOCK
         */
        if (conf->calc_debug & CALCDBG_BLOCK) {
                blkchk(nblk);
        }
        return nblk;
}


/*
 * blk_free - free a block
 *
 * NOTE: THIS IS NOT THE CALC blktrunc() BUILTIN FUNCTION!!  This
 * is what is called to free block storage.
 *
 * given:
 *      blk     - the block to free
 */
void
blk_free(BLOCK *blk)
{
        /* free if non-NULL */
        if (blk != NULL) {

                /* free data storage */
                if (blk->data != NULL) {
                        free(blk->data);
                }

                /* free the block */
                free(blk);
        }
        return;
}


/*
 * blkchk - check the sanity of a block
 *
 * These checks should never fail if calc is working correctly.  During
 * debug time, we plan to call this function often.  Once we are satisfied,
 * we will normally call this code only in a few places.
 *
 * If "calc_debug" has the bit corresponding to CALCDBG_BLOCK set, this
 * function is called during execution of the following builtins:
 *
 *      alloc(), realloc(), free()
 *
 * given:
 *      blk     - the BLOCK to check
 *
 * returns:
 *      if all is OK, otherwise math_error() is called and this
 *      function does not return
 */
S_FUNC void
blkchk(BLOCK *blk)
{

        /*
         * firewall - general sanity check
         */
        if ((conf->calc_debug & CALCDBG_BLOCK) == 0) {
                /* do nothing when debugging is disabled */
                return;
        }
        if (blk == NULL) {
                math_error("internal: blk ptr is NULL");
                not_reached();
        }

        /*
         * pointers must not be NULL
         */
        if (blk->data == NULL) {
                math_error("internal: blk->data ptr is NULL");
                not_reached();
        }

        /*
         * check data lengths
         */
        if (blk->datalen < 0) {
                math_error("internal: blk->datalen < 0");
                not_reached();
        }

        /*
         * check the datalen and datalen2 values
         */
        if (blk->datalen < 0) {
                math_error("internal: blk->datalen < 0");
                not_reached();
        }
        return;
}


/*
 * blkrealloc - reallocate a block
 *
 * Reallocation of a block can change several aspects of a block.
 *
 *      It can change the much data it holds or can hold.
 *
 *      It can change the memory footprint (in terms of
 *      how much storage is malloced for current or future use).
 *
 *      It can change the chunk size used to grow malloced size
 *      as the data size grows.
 *
 * Each of the len and chunksize may be kept the same.
 *
 * given:
 *      blk     - old BLOCK to reallocate
 *      newlen  - how much data the block holds
 *      newchunk - allocation chunk size (<0 ==> no change, 0 == default)
 */
BLOCK *
blkrealloc(BLOCK *blk, int newlen, int newchunk)
{
        USB8 *nblk;     /* realloced storage */
        int newmax;     /* new maximum stoage size */

        /*
         * firewall
         */
        if (conf->calc_debug & CALCDBG_BLOCK) {
                blkchk(blk);
        }

        /*
         * process args
         */
        /* newlen < 0 means do not change the length */
        if (newlen < 0) {
                newlen = blk->datalen;
        }
        /* newchunk <= 0 means do not change the chunk size */
        if (newchunk < 0) {
                newchunk = blk->blkchunk;
        } else if (newchunk == 0) {
                newchunk = BLK_CHUNKSIZE;
        }

        /*
         * reallocate storage if we have a different allocation size
         */
        newmax = ((newlen+newchunk)/newchunk)*newchunk;
        if (newmax != blk->maxsize) {

                /* reallocate new storage */
                nblk = (USB8*)realloc(blk->data, newmax);
                if (nblk == NULL) {
                        math_error("cannot reallocate block storage");
                        not_reached();
                }

                /* clear any new storage */
                if (newmax > blk->maxsize) {
                        memset(nblk+blk->maxsize, 0, (newmax-blk->maxsize));
                }
                blk->maxsize = newmax;

                /* restore the data pointers */
                blk->data = nblk;
        }

        /*
         * deal the case of a newlen == 0 early and return
         */
        if (newlen == 0) {

                /*
                 * setup the empty buffer
                 *
                 * We know that newtype is not circular since we force
                 * newlen to be at least 1 (because circular blocks
                 * always have at least one unused octet).
                 */
                if (blk->datalen < blk->maxsize) {
                        memset(blk->data, 0, blk->datalen);
                } else {
                        memset(blk->data, 0, blk->maxsize);
                }
                blk->datalen = 0;
                if (conf->calc_debug & CALCDBG_BLOCK) {
                        blkchk(blk);
                }
                return blk;
        }

        /*
         * Set the data length
         *
         * We also know that the new block is not empty since we have
         * already dealt with that case above.
         *
         * After this section of code, limit and datalen will be
         * correct in terms of the new type.
         */
        if (newlen > blk->datalen) {

                /* there is new storage, clear it */
                memset(blk->data + blk->datalen, 0, newlen-blk->datalen);
                /* growing storage for blocks grows the data */
                blk->datalen = newlen;

        } else if (newlen <= blk->datalen) {

                /* the block will be full */
                blk->datalen = newlen;
        }

        /*
         * return realloced type
         */
        if (conf->calc_debug & CALCDBG_BLOCK) {
                blkchk(blk);
        }
        return blk;
}


/*
 * blktrunc - truncate a BLOCK down to a minimal fixed block
 *
 * NOTE: THIS IS NOT THE INTERNAL CALC FREE FUNCTION!!  This
 * is what blktrunc() builtin calls to reduce storage of a block
 * down to an absolute minimum.
 *
 * This actually forms a zero length fixed block with a chunk of 1.
 *
 * given:
 *      blk     - the BLOCK to shrink
 *
 * returns:
 *      pointer to a newly allocated BLOCK
 */
void
blktrunc(BLOCK *blk)
{
        /*
         * firewall
         */
        if (conf->calc_debug & CALCDBG_BLOCK) {
                blkchk(blk);
        }

        /*
         * free the old storage
         */
        free(blk->data);

        /*
         * setup as a zero length fixed block
         */
        blk->blkchunk = 1;
        blk->maxsize = 1;
        blk->datalen = 0;
        blk->data = (USB8*)malloc(1);
        if (blk->data == NULL) {
                math_error("cannot allocate truncated block storage");
                not_reached();
        }
        blk->data[0] = (USB8)0;
        if (conf->calc_debug & CALCDBG_BLOCK) {
                blkchk(blk);
        }
        return;
}


/*
 * blk_copy - copy a block
 *
 * given:
 *      blk     - the block to copy
 *
 * returns:
 *      pointer to copy of blk
 */
BLOCK *
blk_copy(BLOCK *blk)
{
        BLOCK *nblk;            /* copy of blk */

        /*
         * malloc new block
         */
        nblk = (BLOCK *)malloc(sizeof(BLOCK));
        if (nblk == NULL) {
                math_error("blk_copy: cannot malloc BLOCK");
                not_reached();
        }

        /*
         * duplicate most of the block
         */
        *nblk = *blk;

        /*
         * duplicate block data
         */
        nblk->data = (USB8 *)malloc(blk->maxsize);
        if (nblk->data == NULL) {
                math_error("blk_copy: cannot duplicate block data");
                not_reached();
        }
        memcpy(nblk->data, blk->data, blk->maxsize);
        return nblk;
}


/*
 * blk_cmp - compare blocks
 *
 * given:
 *      a       first BLOCK
 *      b       second BLOCK
 *
 * returns:
 *      true => BLOCKs are different
 *      false => BLOCKs are the same
 */
int
blk_cmp(BLOCK *a, BLOCK *b)
{
        /*
         * firewall and quick check
         */
        if (a == b) {
                /* pointers to the same object */
                return false;
        }
        if (a == NULL || b == NULL) {
                /* one pointer is NULL, so they differ */
                return true;
        }

        /*
         * compare lengths
         */
        if (a->datalen != b->datalen) {
                /* different lengths are different */
                return true;
        }

        /*
         * compare the section
         *
         * We have the same lengths and types, so compare the data sections.
         */
        if (memcmp(a->data, b->data, a->datalen) != 0) {
                /* different sections are different */
                return true;
        }

        /*
         * the blocks are the same
         */
        return false;
}


/*
 * Print chunksize, maxsize, datalen on line line and if datalen > 0,
 * up to * 30 octets on the following line, with ... if datalen exceeds 30.
 */
/*ARGSUSED*/
void
blk_print(BLOCK *blk)
{
        long i;
        bool havetail;
        USB8 *ptr;

        /* XXX - should use the config parameters for better print control */

        printf("chunksize = %d, maxsize = %d, datalen = %d\n\t",
                (int)blk->blkchunk, (int)blk->maxsize, (int)blk->datalen);
        i = blk->datalen;
        havetail = (i > 30);
        if (havetail)
                i = 30;
        ptr = blk->data;
        while (i-- > 0)
                printf("%02x", *ptr++);
        if (havetail)
                printf("...");
}


/*
 * Routine to print id and name of a named block and details of its
 * block component.
 */
void
nblock_print(NBLOCK *nblk)
{
        BLOCK *blk;

        /* XXX - use the config parameters for better print control */

        blk = nblk->blk;
        printf("block %d: %s\n\t", nblk->id, nblk->name);
        if (blk->data == NULL) {
                printf("chunksize = %d, maxsize = %d, datalen = %d\n\t",
                (int)blk->blkchunk, (int)blk->maxsize, (int)blk->datalen);
                printf("NULL");
        } else {
                blk_print(blk);
        }
}


/*
 * realloc a named block specified by its id.  The new datalen and
 * chunksize are specified by len >= 0 and chunk > 0.  If len < 0
 * or chunk <= 0, these values used are the current datalen and
 * chunksize, so there is no point in calling this unless len >= 0
 * and/or chunk > 0.
 * No reallocation occurs if the new maxsize is equal to the old maxsize.
 */
NBLOCK *
reallocnblock(int id, int len, int chunk)
{
        BLOCK *blk;
        int newsize;
        int oldsize;
        USB8* newdata;

        /* Fire wall */
        if (id < 0 || id >= nblockcount) {
                math_error("Bad id in call to reallocnblock");
                not_reached();
        }

        blk = nblocks[id]->blk;
        if (len < 0)
                len = blk->datalen;
        if (chunk < 0)
                chunk = blk->blkchunk;
        else if (chunk == 0)
                chunk = BLK_CHUNKSIZE;
        newsize = (1 + len/chunk) * chunk;
        oldsize = blk->maxsize;
        newdata = blk->data;
        if (newdata == NULL) {
                newdata = malloc(newsize);
                if (newdata == NULL) {
                        math_error("Allocation failed");
                        not_reached();
                }
        } else if (newsize != oldsize) {
                newdata = realloc(blk->data, newsize);
                if (newdata == NULL) {
                        math_error("Reallocation failed");
                        not_reached();
                }
        }
        memset(newdata + len, 0, newsize - len);

        blk->maxsize = newsize;
        blk->datalen = len;
        blk->blkchunk = chunk;
        blk->data = newdata;
        return nblocks[id];
}


/*
 * Create and return a new namedblock with specified name, len and
 * chunksize.
 */
NBLOCK *
createnblock(char *name, int len, int chunk)
{
        NBLOCK *res;
        char *newname;

        if (nblockcount >= maxnblockcount) {
                if (maxnblockcount <= 0) {
                        maxnblockcount = NBLOCKCHUNK;
                        nblocks = (NBLOCK **)malloc(NBLOCKCHUNK *
                                                    sizeof(NBLOCK *));
                        if (nblocks == NULL) {
                                maxnblockcount = 0;
                                math_error("unable to malloc new named blocks");
                                not_reached();
                        }
                } else {
                        maxnblockcount += NBLOCKCHUNK;
                        nblocks = (NBLOCK **)realloc(nblocks, maxnblockcount *
                                                     sizeof(NBLOCK *));
                        if (nblocks == NULL) {
                                maxnblockcount = 0;
                                math_error("cannot malloc more named blocks");
                                not_reached();
                        }
                }
        }
        if (nblockcount == 0)
                initstr(&nblocknames);
        if (findstr(&nblocknames, name) >= 0) {
                math_error("Named block already exists!!!");
                not_reached();
        }
        newname = addstr(&nblocknames, name);
        if (newname == NULL) {
                math_error("Block name allocation failed");
                not_reached();
        }

        res = (NBLOCK *) malloc(sizeof(NBLOCK));
        if (res == NULL) {
                math_error("Named block allocation failed");
                not_reached();
        }

        nblocks[nblockcount] = res;
        res->name = newname;
        res->subtype = V_NOSUBTYPE;
        res->id = nblockcount++;
        res->blk = blkalloc(len, chunk);
        return res;
}


/*
 * find a named block
 */
int
findnblockid(char * name)
{
        return findstr(&nblocknames, name);
}


/*
 * free data block for named block with specified id
 */
int
removenblock(int id)
{
        NBLOCK *nblk;

        if (id < 0 || id >= nblockcount)
                return E_BLKFREE_3;
        nblk = nblocks[id];
        if (nblk->blk->data == NULL)
                return 0;
        if (nblk->subtype & V_NOREALLOC)
                return E_BLKFREE_5;
        free(nblk->blk->data);
        nblk->blk->data = NULL;
        nblk->blk->maxsize = 0;
        nblk->blk->datalen = 0;
        return 0;
}


/*
 * count number of current unfreed named blocks
 */
int
countnblocks(void)
{
        int n;
        int id;

        for (n = 0, id = 0; id < nblockcount; id++) {
                if (nblocks[id]->blk->data != NULL)
                        n++;
        }
        return n;
}


/*
 * display id and name for each unfreed named block
 */
void
shownblocks(void)
{
        int id;

        if (countnblocks() == 0) {
                printf("No unfreed named blocks\n\n");
                return;
        }
        printf(" id   name\n");
        printf("----  -----\n");
        for (id = 0; id < nblockcount; id++) {
                if (nblocks[id]->blk->data != NULL)
                        printf("%3d   %s\n", id, nblocks[id]->name);
        }
        printf("\n");
}


/*
 * Return pointer to nblock with specified id, NULL if never created.
 * The memory for the nblock found may have been freed.
 */
NBLOCK *
findnblock(int id)
{
        if (id < 0 || id >= nblockcount)
                return NULL;
        return nblocks[id];
}


/*
 * Create a new block with specified newlen and new chunksize and copy
 * min(newlen, oldlen) octets to the new block.  The old block is
 * not changed.
 */
BLOCK *
copyrealloc(BLOCK *blk, int newlen, int newchunk)
{
        BLOCK * newblk;
        int oldlen;

        oldlen = blk->datalen;

        if (newlen < 0)                 /* retain length */
                newlen = oldlen;

        if (newchunk < 0)               /* retain chunksize */
                newchunk = blk->blkchunk;
        else if (newchunk == 0)         /* use default chunksize */
                newchunk = BLK_CHUNKSIZE;


        newblk = blkalloc(newlen, newchunk);

        if (newlen < oldlen)
                oldlen = newlen;

        if (newlen > 0)
                memcpy(newblk->data, blk->data, oldlen);

        return newblk;
}
