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

#include "os.h"

#include "iniread.h"

#define false 0
#define true 1

IniString *IniString_append(IniString *this, char ch)
{
    char *oldbuffer = this->buffer;
    int oldlen = (oldbuffer ? strlen(this->buffer) : 0);
    this->buffer = malloc(oldlen + 2);
    if (oldlen) memmove(this->buffer, oldbuffer, oldlen);
    if (oldbuffer) free(oldbuffer);
    this->buffer[oldlen] = ch;
    this->buffer[oldlen + 1] = 0;
    return this;
}

void IniString_copy(IniString *dst, const IniString *src)
{
    if (dst->buffer) free(dst->buffer);

    if (src->buffer)
    {
        dst->buffer = malloc(strlen(src->buffer) + 1);
        strcpy(dst->buffer, src->buffer);
    }
    else
        dst->buffer = NULL;
}

bool IniString_is_null(const IniString *this)
{
    return (!this->buffer || !this->buffer[0] || strcmp(this->buffer, "0") == 0);
}

void IniString_assign(IniString *this, const char *s)
{
    if (this->buffer) free(this->buffer);
    this->buffer = malloc(strlen(s)+1);
    strcpy(this->buffer, s);
}

int strcasecmp(const char *s1, const char *s2)
{
	while (*s1 && *s2) {
		if (toupper(*s1) != toupper(*s2)) {
			return (*s1 < *s2) ? -1 : 1;
		}
		s1++;
		s2++;
	}
	return *s2 ? -1 : (*s1 ? 1 : 0);
}

bool IniString_equals(const IniString *s1, const IniString *s2)
{
    if (IniString_is_null(s1) && IniString_is_null(s2)) return true;
    else if (s1->buffer && s2->buffer && strcasecmp(s1->buffer, s2->buffer) == 0) return true;
    else return false;
}

bool IniString_need_quotes(IniString *this)
{
    return (this->buffer &&
            (strchr(this->buffer, ' ') || strchr(this->buffer, ',')));
}

void IniStringList_destroy(IniStringList *this)
{
    int i;

    for (i=0; i<this->count; i++)
    {
        IniString_destroy(&this->list[i]);
    }

    free(this->list);

    this->list=NULL;

    this->count=0;
}

void IniStringList_Erase(IniStringList *this)
{
    int i;

    for (i=0; i<this->count; i++)
    {
        IniString_destroy(&this->list[i]);
    }

    free(this->list);

    this->list=NULL;

    this->count=-1;
}

void IniStringList_Add(IniStringList *this, const IniString *str)
{
    IniString *newlist;

    if (this->count < 0) this->count = 0;

    newlist = calloc(this->count + 1, sizeof(IniString));

    if (this->list)
    {
        int i;

        for (i = 0; i < this->count; i++)
        {
            IniString_copy(&newlist[i], &this->list[i]);
            IniString_destroy(&this->list[i]);
        }
        free(this->list);
    }
    IniString_copy(&newlist[this->count++], str);

    this->list = newlist;
}

void IniStringList_copy(IniStringList *dst, const IniStringList *src)
{
    IniStringList_destroy(dst);
    if ((dst->count = src->count) != 0)
    {
        int i;

        dst->list = calloc(dst->count, sizeof(IniString));

        for (i = 0; i < dst->count; i++)
            IniString_copy(&dst->list[i], &src->list[i]);
    }
    else
        dst->list = NULL;
}

void IniStringList_fwrite(IniStringList *this, FILE *out)
{
    int i;

    for (i=0; i < IniStringList_length(this); i++)
    {
        if (i) fprintf(out, ",");

        if (IniString_need_quotes(&this->list[i]))
            fprintf(out, "\"%s\"", IniString_c_str(&this->list[i]));
        else
            fprintf(out, "%s", IniString_c_str(&this->list[i]));
    }
}

void IniRecord_init(IniRecord *this)
{
    this->flags = 0;
    this->next = NULL;
    IniString_init(&this->key);
    IniStringList_init(&this->values);
}

void IniRecord_destroy(IniRecord *this)
{
    IniString_destroy(&this->key);
    IniStringList_destroy(&this->values);
    if (this->next)
        IniRecord_destroy(this->next);
}

IniRecord *IniRecord_findfirst(IniRecord *this, const char *Key)
{
    IniRecord *ptr;

    if (Key == NULL) return this;

    ptr = this;
    while (ptr)
    {
        if (strcmp(IniString_c_str(&ptr->key), Key) == 0) return ptr;
        ptr = ptr->next;
    }

    return NULL;
}

void IniRecord_fwrite(IniRecord *this, FILE *out)
{
    if (IniStringList_length(&this->values) < 0) return;

    if (IniString_need_quotes(&this->key))
    	fprintf(out, "\"%s\"=", IniString_c_str(&this->key));
    else
    	fprintf(out, "%s=", IniString_c_str(&this->key));

    IniStringList_fwrite(&this->values, out);
    fprintf(out, "\n");
}

void IniSection_init(IniSection *this)
{
    this->lastrecord = NULL;
    this->next = NULL;
    this->record = NULL;
    IniString_init(&this->section);
}

void IniSection_destroy(IniSection *this)
{
    IniString_destroy(&this->section);
    if (this->record)
    {
        IniRecord_destroy(this->record);
        free(this->record);
        this->record=NULL;
    }
    if (this->next)
    {
        IniSection_destroy(this->next);
        free(this->next);
        this->next=NULL;
    }
}

IniRecord *IniSection_addrecord(IniSection *this, const IniString *Section, const IniString *Key, const IniStringList *Values)
{
    if (IniString_equals(&this->section, Section) ||
        (!this->record && IniString_is_null(&this->section)))
    {
        IniString_copy(&this->section, Section);
        if (this->record && this->lastrecord)
        {
            this->lastrecord->next = malloc(sizeof(IniRecord));
            IniRecord_init(this->lastrecord->next);
            IniString_copy(&this->lastrecord->next->key, Key);
            IniStringList_copy(&this->lastrecord->next->values, Values);
            this->lastrecord = this->lastrecord->next;
        }
        else
        {
            this->lastrecord = this->record = malloc(sizeof(IniRecord));
            IniRecord_init(this->record);
            IniString_copy(&this->record->key, Key);
            IniStringList_copy(&this->record->values, Values);
        }

        return this->lastrecord;
    }
    else if (this->next == NULL)
    {
        this->next = malloc(sizeof(IniSection));
        IniSection_init(this->next);
    }

    return IniSection_addrecord(this->next, Section, Key, Values);
}

bool IniSection_ReadIniFile(IniSection *this, const char *Filename, const char *Section_c, long offset)
{
    FILE *inf = fopen(Filename, "rb");
    IniString sect, Section;

    if (inf == NULL) return true;
    if (offset < 0) fseek(inf, offset, SEEK_END);
    if (offset > 0) fseek(inf, offset, SEEK_SET);

    IniString_init(&sect);
    IniString_init(&Section);

    if (Section_c)
        IniString_assign(&Section, Section_c);

    while (!feof(inf))
    {
        /* eat leading whitespace */
        int peekch = fgetc(inf);
        while (peekch != EOF && isspace(peekch)) peekch = fgetc(inf);

        if (peekch == '[')
        {
            IniString h;

            IniString_init(&h);

            /* Handle section headings */
            while ((peekch = fgetc(inf)) != EOF &&
                   peekch != '\n' && peekch != ']' &&
                   peekch != ';')
                if (isprint(peekch))
                        IniString_append(&h, (char)peekch);

            if (peekch == ']') IniString_copy(&sect, &h);

            while (peekch != EOF && peekch != '\n') peekch = fgetc(inf);

            IniString_destroy(&h);
        }
        else
        {
            /* Handle equating lines */
            IniString key;
            char *p;

            IniString_init(&key);

            ungetc(peekch, inf);

            while ((peekch = fgetc(inf)) != EOF && peekch != '\n' &&
                   peekch != '=' && peekch != ';')
		if (isprint(peekch)) IniString_append(&key, (char)peekch);

            if (!IniString_is_null(&Section) && !IniString_equals(&sect, &Section)) continue;

            /* chop trailing space */
            p = strchr(IniString_c_str(&key), 0) - 1;
            while (isspace(*p) && p >= IniString_c_str(&key)) *p-- = 0;

            /* separate out all of the values */
            if (peekch == '=')
            {
                IniStringList args;
                IniStringList_init(&args);

                /* strip out one argument */
                while (!feof(inf))
                {
                    IniString value;
                    int quoted;

                    IniString_init(&value);

                    while ((peekch = fgetc(inf)) != EOF)
                        if (peekch == '\n' || !isspace(peekch))
                        {
                            ungetc(peekch, inf);
                            break;
                        }

                    quoted = (peekch == '"');
                    if (quoted) fgetc(inf);

                    while ((peekch = fgetc(inf)) != EOF)
                    {
                        if (quoted && peekch == '"')
                        {
                            while ((peekch = fgetc(inf)) != EOF &&
                                   peekch != ',' && peekch != '\n' &&
                                   peekch != ';');
                            break;
                        }
                        else if (quoted && peekch == '\n')
                        {
                            break;
                        }
                        else if (!quoted && (peekch == ',' || peekch == ';' | peekch == '\n'))
                        {
                            /* strip trailing whitespace */
                            char *p = strchr(IniString_c_str(&value), 0) - 1;
                            while (isspace(*p) && p >= IniString_c_str(&value)) *p-- = 0;
                            break;
                        }
                        else
                        {
                            if (isprint(peekch)) IniString_append(&value, peekch);
                        }
                    }

                    /* add it to our chain */
                    IniStringList_Add(&args, &value);

                    IniString_destroy(&value);

                    /* if we stopped because of a comment, then ignore to EOL */
                    if (peekch == ';') while ((peekch = fgetc(inf)) != EOF && peekch != '\n');

                    /* break if this was the end of the line */
                    if (peekch == '\n') break;
                }

                IniSection_addrecord(this, &sect, &key, &args);

                IniStringList_destroy(&args);
            }
            else if (peekch == ';')
            {
                /* if we stopped because of a comment, then ignore to EOL */
                while ((peekch = fgetc(inf)) != EOF && peekch != '\n');
            }

            IniString_destroy(&key);
        }
    }

    IniString_destroy(&sect);
    IniString_destroy(&Section);

    fclose(inf);
    return false;
}

IniRecord *IniSection_findfirst(const IniSection *this, const char *Section, const char *Key)
{
    const IniSection *ptr = this;

    while (ptr)
    {
        if (strcasecmp(IniString_c_str(&ptr->section), Section) == 0)
        {
            if (ptr->record)
                return IniRecord_findfirst(ptr->record, Key);
            else
                return NULL;
        }
        else if (Section == NULL && ptr->record)
        {
            IniRecord *that = IniRecord_findfirst(ptr->record, Key);
            if (that) return that;
        }
        ptr = ptr->next;
    }

    return NULL;
}

IniStringList *IniSection_getkey(IniSection *this, const char *Section, const char *Key, const char *DefValue, bool AutoAdd)
{

    IniRecord *r = IniSection_findfirst(this, Section, Key);
    if (r) return &r->values;
    else
    {
        if (AutoAdd)
        {
            IniStringList *ret;
            IniString s, k, d;
            IniStringList dl;
            IniString_init(&s); IniString_init(&k); IniString_init(&d); IniStringList_init(&dl);
            if (Section) IniString_assign(&s, Section);
            if (Key) IniString_assign(&k, Key);
            if (DefValue) IniString_assign(&d, DefValue);
            IniStringList_Add(&dl, &d);
            ret = &IniSection_addrecord(this, &s, &k, &dl)->values;
            IniString_destroy(&s); IniString_destroy(&k); IniString_destroy(&d); IniStringList_destroy(&dl);
            return ret;
        }
        else
        {
            static IniStringList dummy;
            IniString d;
            IniString_init(&d);
            IniString_assign(&d, DefValue);
            IniStringList_destroy(&dummy);
            IniStringList_Add(&dummy, &d);
            IniString_destroy(&d);
            return &dummy;
        }
    }
}

IniRecord *IniSection_setrecord(IniSection *this, const char *Section, const char *Key, const char *Value)
{
    if (strcasecmp(IniString_c_str(&this->section), Section) == 0 ||
          !this->record && IniString_is_null(&this->section))
    {
        IniRecord *ptr;

        if (strcasecmp(IniString_c_str(&this->section), Section))
            IniString_assign(&this->section, Section);

        ptr = IniRecord_findfirst(this->record, Key);
        if (ptr)
        {
            IniString v;
            IniString_init(&v);
            IniString_assign(&v, Value);

            IniStringList_destroy(&ptr->values);
            IniStringList_Add(&ptr->values, &v);

            IniString_destroy(&v);

            return ptr;
        }
        else
        {
            if (this->record && this->lastrecord)
            {
            	IniString v;
            	IniString_init(&v);
            	IniString_assign(&v, Value);

                this->lastrecord->next = malloc(sizeof(IniRecord));
                IniRecord_init(this->lastrecord->next);
                IniString_assign(&this->lastrecord->next->key, Key);
                IniStringList_Add(&this->lastrecord->next->values, &v);
                IniString_destroy(&v);
                this->lastrecord = this->lastrecord->next;
            }
            else
            {
            	IniString v;
            	IniString_init(&v);
            	IniString_assign(&v, Value);

                this->lastrecord = this->record = malloc(sizeof(IniRecord));
                IniRecord_init(this->record);
                IniString_assign(&this->record->key, Key);
                IniStringList_Add(&this->record->values, &v);
                IniString_destroy(&v);
            }

            return this->lastrecord;
        }
    }
    else if (this->next == NULL)
    {
        this->next = malloc(sizeof(IniSection));
        IniSection_init(this->next);
    }

    return IniSection_setrecord(this->next, Section, Key, Value);
}

void IniSection_fwrite(IniSection *this, FILE *out)
{
    IniRecord *r = this->record;

    if (!IniString_is_null(&this->section) || this->record)
    	fprintf(out, "[%s]\n", IniString_c_str(&this->section));

    while (r)
    {
        IniRecord_fwrite(r, out);
        r = r->next;
    }

    fprintf(out, "\n");
    if (this->next) IniSection_fwrite(this->next, out);
}

bool IniSection_WriteIniFile(IniSection *this, const char *filename)
{
    FILE *outf = fopen(filename, "wt");
    if (outf == NULL) return true;
    IniSection_fwrite(this, outf);
    fclose(outf);
    return false;
}
