/*
    LumpMod v0.2.1, a command-line utility for working with lumps in wad
                    files.
    Copyright (C) 2003 Thunder Palace Entertainment.

    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.

    lump.c: Provides functions for dealing with lumps
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "lump.h"

/* Read contents of a wad file and store them in memory.
 * fpoint is the file to read, opened with "rb" mode.
 * A pointer to a new wadfile struct will be returned, or NULL on error.
 */
struct wadfile *read_wadfile(FILE *fpoint) {
    struct wadfile *wfptr;
    struct lumplist *curlump;
    long diroffset, filelen;
    unsigned long count;

    /* Allocate memory for wadfile struct */
    wfptr = malloc(sizeof(struct wadfile));
    if(wfptr == NULL) return NULL;

    /* Read first four characters (PWAD or IWAD) */
    if(fread(wfptr->id, 4, 1, fpoint) < 1) {
        free(wfptr);
        return NULL;
    }

    /* Read number of lumps */
    if(fread(&(wfptr->numlumps), 4, 1, fpoint) < 1) {
        free(wfptr);
        return NULL;
    }

    /* If number of lumps is zero, nothing more needs to be done */
    if(wfptr->numlumps == 0) {
        wfptr->head = NULL;
        return wfptr;
    }

    /* Read offset of directory */
    if(fread(&diroffset, 4, 1, fpoint) < 1) {
        free(wfptr);
        return NULL;
    }

    /* Verify that the directory as long as it needs to be */
    fseek(fpoint, 0, SEEK_END);
    filelen = ftell(fpoint);
    if((filelen - diroffset) / DIRENTRYLEN < wfptr->numlumps) {
        free(wfptr);
        return NULL;
    }

    /* Allocate memory for head lumplist item and set head pointer */
    curlump = malloc(sizeof(struct lumplist));
    if(curlump == NULL) {
        free(wfptr);
        return NULL;
    }
    wfptr->head = curlump;
    curlump->cl = NULL;

    /* Read directory entries and lumps */
    for(count = 0; count < wfptr->numlumps; count++) {
        long lumpdataoffset;

        /* Advance to a new list item */
        curlump->next = malloc(sizeof(struct lumplist));
        if(curlump->next == NULL) {
            free_wadfile(wfptr);
            return NULL;
        }
        curlump = curlump->next;
        curlump->next = NULL;

        /* Allocate memory for the lump info */
        curlump->cl = malloc(sizeof(struct lump));
        if(curlump->cl == NULL) {
            free_wadfile(wfptr);
            return NULL;
        }

        /* Seek to the proper position in the file */
        if(fseek(fpoint, diroffset + (count * DIRENTRYLEN), SEEK_SET) != 0) {
            free_wadfile(wfptr);
            return NULL;
        }

        /* Read offset of lump data */
        if(fread(&lumpdataoffset, 4, 1, fpoint) < 1) {
            free_wadfile(wfptr);
            return NULL;
        }

        /* Read size of lump in bytes */
        if(fread(&(curlump->cl->len), 4, 1, fpoint) < 1) {
            free_wadfile(wfptr);
            return NULL;
        }

        /* Read lump name */
        if(fread(curlump->cl->name, 8, 1, fpoint) < 1) {
            free_wadfile(wfptr);
            return NULL;
        }

        /* Read actual lump data, unless lump size is 0 */
        if(curlump->cl->len > 0) {
            if(fseek(fpoint, lumpdataoffset, SEEK_SET) != 0) {
                free_wadfile(wfptr);
                return NULL;
            }

            /* Allocate memory for data */
            curlump->cl->data = malloc(curlump->cl->len);
            if(curlump->cl->data == NULL) {
                free_wadfile(wfptr);
                return NULL;
            }

            /* Fill the data buffer */
            if(fread(curlump->cl->data, curlump->cl->len, 1, fpoint) < 1) {
                free_wadfile(wfptr);
                return NULL;
            }
        } else curlump->cl->data = NULL;
    } /* End of directory reading loop */

    return wfptr;
}

/* Free a wadfile from memory as well as all related structures.
 */
void free_wadfile(struct wadfile *wfptr) {
    struct lumplist *curlump, *nextlump;

    if(wfptr == NULL) return;
    curlump = wfptr->head;

    /* Free items in the lump list */
    while(curlump != NULL) {

        /* Free the actual lump and its data, if necessary */
        if(curlump->cl != NULL) {
            if(curlump->cl->data != NULL) free(curlump->cl->data);
            free(curlump->cl);
        }

        /* Advance to next lump and free this one */
        nextlump = curlump->next;
        free(curlump);
        curlump = nextlump;
    }

    free(wfptr);
}

/* Write complete wadfile to a file stream, opened with "wb" mode.
 * fpoint is the stream to write to.
 * wfptr is a pointer to the wadfile structure to use.
 * Return zero on success, nonzero on failure.
 */
int write_wadfile(FILE *fpoint, struct wadfile *wfptr) {
    struct lumplist *curlump;
    long lumpdataoffset, diroffset;

    if(wfptr == NULL) return 1;

    /* Write four-character ID ("PWAD" or "IWAD") */
    if(fwrite(wfptr->id, 4, 1, fpoint) < 1) return 2;

    /* Write number of lumps */
    if(fwrite(&(wfptr->numlumps), 4, 1, fpoint) < 1) return 3;

    /* Offset of directory is not known yet. For now, write number of lumps
     * again, just to fill the space.
     */
    if(fwrite(&(wfptr->numlumps), 4, 1, fpoint) < 1) return 4;

    /* Loop through lump list, writing lump data */
    for(curlump = wfptr->head; curlump != NULL; curlump = curlump->next) {

        /* Don't write anything for the head of the lump list or for lumps of
           zero length */
        if(curlump->cl == NULL || curlump->cl->data == NULL) continue;

        /* Write the data */
        if(fwrite(curlump->cl->data, curlump->cl->len, 1, fpoint) < 1)
            return 5;
    }

    /* Current position is where directory will start */
    diroffset = ftell(fpoint);

    /* Offset for the first lump's data is always 12 */
    lumpdataoffset = 12;

    /* Loop through lump list again, this time writing directory entries */
    for(curlump = wfptr->head; curlump != NULL; curlump = curlump->next) {

        /* Don't write anything for the head of the lump list */
        if(curlump->cl == NULL) continue;

        /* Write offset for lump data */
        if(fwrite(&lumpdataoffset, 4, 1, fpoint) < 1) return 6;

        /* Write size of lump data */
        if(fwrite(&(curlump->cl->len), 4, 1, fpoint) < 1) return 7;

        /* Write lump name */
        if(fwrite(curlump->cl->name, 8, 1, fpoint) < 1) return 8;

        /* Increment lumpdataoffset variable as appropriate */
        lumpdataoffset += curlump->cl->len;
    }

    /* Go back to header and write the proper directory offset */
    fseek(fpoint, 8, SEEK_SET);
    if(fwrite(&diroffset, 4, 1, fpoint) < 1) return 9;

    return 0;
}

/* Get the name of a lump, as a null-terminated string.
 * item is a pointer to the lump (not lumplist) whose name will be obtained.
 * Return NULL on error.
 */
char *get_lump_name(struct lump *item) {
    char convname[9], *retname;

    if(item == NULL) return NULL;
    memcpy(convname, item->name, 8);
    convname[8] = '\0';

    retname = malloc(strlen(convname) + 1);
    if(retname != NULL) strcpy(retname, convname);
    return retname;
}

/* Find the lump after start and before end having a certain name.
 * Return a pointer to the list item for that lump, or return NULL if no lump
 * by that name is found or lumpname is too long.
 * lumpname is a null-terminated string.
 * If end parameter is NULL, search to the end of the entire list.
 */
struct lumplist *find_previous_lump(struct lumplist *start, struct lumplist
        *end, char *lumpname) {
    struct lumplist *curlump, *lastlump;
    char *curname;
    int found = 0;

    /* Verify that parameters are valid */
    if(start==NULL || start==end || lumpname==NULL || strlen(lumpname) > 8)
        return NULL;

    /* Loop through the list from start parameter */
    lastlump = start;
    for(curlump = start->next; curlump != end && curlump != NULL;
            curlump = curlump->next) {

        /* Skip header lump */
        if(curlump->cl == NULL) continue;

        /* Find name of this lump */
        curname = get_lump_name(curlump->cl);
        if(curname == NULL) continue;

        /* Compare names to see if this is the lump we want */
        if(strcmp(curname, lumpname) == 0) {
            found = 1;
            break;
        }

        /* Free memory allocated to curname */
        free(curname);

        lastlump = curlump;
    }

    if(found) return lastlump;
    return NULL;
}

/* Remove a lump from the list, free it, and free its data.
 * before is the lump immediately preceding the lump to be removed.
 * wfptr is a pointer to the wadfile structure to which the removed lump
 * belongs, so that numlumps can be decreased.
 */
void remove_next_lump(struct wadfile *wfptr, struct lumplist *before) {
    struct lumplist *removed;

    /* Verify that parameters are valid */
    if(before == NULL || before->next == NULL || wfptr == NULL) return;

    /* Update linked list to omit removed lump */
    removed = before->next;
    before->next = removed->next;

    /* Free lump info and data if necessary */
    if(removed->cl != NULL) {
        if(removed->cl->data != NULL) free(removed->cl->data);
        free(removed->cl);
    }

    free(removed);

    /* Decrement numlumps */
    wfptr->numlumps--;
}

/* Add a lump.
 * The lump will follow prev in the list and be named name, with a data size
 * of len.
 * A copy will be made of the data.
 * Return zero on success or nonzero on failure.
 */
int add_lump(struct wadfile *wfptr, struct lumplist *prev, char *name, long
        len, unsigned char *data) {
    struct lump *newlump;
    struct lumplist *newlumplist;
    unsigned char *copydata;

    /* Verify that parameters are valid */
    if(wfptr == NULL || prev == NULL || name == NULL || strlen(name) > 8)
        return 1;

    /* Allocate space for newlump and newlumplist */
    newlump = malloc(sizeof(struct lump));
    newlumplist = malloc(sizeof(struct lumplist));
    if(newlump == NULL || newlumplist == NULL) return 2;

    /* Copy lump data and set up newlump */
    if(len == 0 || data == NULL) {
        newlump->len = 0;
        newlump->data = NULL;
    } else {
        newlump->len = len;
        copydata = malloc(len);
        if(copydata == NULL) return 3;
        memcpy(copydata, data, len);
        newlump->data = copydata;
    }

    /* Set name of newlump */
    memset(newlump->name, '\0', 8);
    if(strlen(name) == 8) memcpy(newlump->name, name, 8);
    else strcpy(newlump->name, name);

    /* Set up newlumplist and alter prev appropriately */
    newlumplist->cl = newlump;
    newlumplist->next = prev->next;
    prev->next = newlumplist;

    /* Increment numlumps */
    wfptr->numlumps++;

    return 0;
}

/* Rename a lump.
 * renamed is a pointer to the lump (not lumplist) that needs renaming.
 * newname is a null-terminated string with the new name.
 * Return zero on success or nonzero on failure.
 */
int rename_lump(struct lump *renamed, char *newname) {

    /* Verify that parameters are valid. */
    if(newname == NULL || renamed == NULL || strlen(newname) > 8) return 1;

    /* Do the renaming. */
    memset(renamed->name, '\0', 8);
    if(strlen(newname) == 8) memcpy(renamed->name, newname, 8);
    else strcpy(renamed->name, newname);

    return 0;
}

/* Find the last lump in a wadfile structure.
 * Return this lump or NULL on failure.
 */
struct lumplist *find_last_lump(struct wadfile *wfptr) {
    struct lumplist *curlump;

    if(wfptr == NULL || wfptr->head == NULL) return NULL;
    curlump = wfptr->head;

    while(curlump->next != NULL) curlump = curlump->next;
    return curlump;
}

/* Find the last lump between start and end.
 * Return this lump or NULL on failure.
 */
struct lumplist *find_last_lump_between(struct lumplist *start, struct
        lumplist *end) {
    struct lumplist *curlump;

    if(start == NULL) return NULL;
    curlump = start;

    while(curlump->next != end) {
        curlump = curlump->next;
        if(curlump == NULL) break;
    }

    return curlump;
}

/* Find the next section lump. A section lump is MAPxx (0 <= x <= 9), ExMy
 * (0 <= x <= 9, 0 <= y <= 9), or any lump whose name ends in _START or _END.
 * Return NULL if there are no section lumps after start.
 */
struct lumplist *find_next_section_lump(struct lumplist *start) {
    struct lumplist *curlump, *found = NULL;
    char *curname;

    /* Verify that parameter is valid */
    if(start == NULL || start->next == NULL) return NULL;

    /* Loop through the list from start parameter */
    for(curlump = start->next; curlump != NULL && found == NULL;
            curlump = curlump->next) {

        /* Skip header lump */
        if(curlump->cl == NULL) continue;

        /* Find name of this lump */
        curname = get_lump_name(curlump->cl);
        if(curname == NULL) continue;

        /* Check to see if this is a section lump */
        if(strlen(curname) == 5 && strncmp("MAP", curname, 3) == 0 &&
                isdigit(curname[3]) && isdigit(curname[4]))
            found = curlump;
        else if(strlen(curname) == 4 && curname[0] == 'E' && curname[2] ==
                'M' && isdigit(curname[1]) && isdigit(curname[3]))
            found = curlump;
        else if(strlen(curname) == 7 && strcmp("_START", &curname[1]) == 0)
            found = curlump;
        else if(strlen(curname) == 8 && strcmp("_START", &curname[2]) == 0)
            found = curlump;
        else if(strlen(curname) == 5 && strcmp("_END", &curname[1]) == 0)
            found = curlump;
        else if(strlen(curname) == 6 && strcmp("_END", &curname[2]) == 0)
            found = curlump;

        /* Free memory allocated to curname */
        free(curname);
    }

    return found;
}