// Copyright (C) 2006-2014 David Sugar, Tycho Softworks.
// Copyright (C) 2015-2020 Cherokees of Idaho.
//
// This file is part of GNU uCommon C++.
//
// GNU uCommon C++ is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU uCommon C++ 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.
//
// You should have received a copy of the GNU Lesser General Public License
// along with GNU uCommon C++.  If not, see <http://www.gnu.org/licenses/>.

#include <ucommon-config.h>
#include <ucommon/export.h>
#include <ucommon/linked.h>
#include <ucommon/memory.h>
#include <ucommon/keydata.h>
#include <ucommon/string.h>
#include <ctype.h>

namespace ucommon {

keydata::keyvalue::keyvalue(keyfile *allocator, keydata *section, const char *kv, const char *dv) :
OrderedObject(&section->index)
{
    assert(allocator != NULL);
    assert(section != NULL);
    assert(kv != NULL);

    id = allocator->dup(kv);

    if(dv)
        value = allocator->dup(dv);
    else
        value = "";
}

keydata::keydata(keyfile *file, const char *id) :
OrderedObject(&file->index), index()
{
    assert(file != NULL);
    assert(id != NULL);

    name = file->dup(id);
    root = file;
}

keydata::keydata(keyfile *file) :
OrderedObject(), index()
{
    root = file;
    name = "-";
}

const char *keydata::get(const char *key) const
{
    assert(key != NULL);

    keydata::pointer keys = begin();

    while(is(keys)) {
        if(eq_case(key, keys->id))
            return keys->value;
        keys.next();
    }
    return NULL;
}

void keydata::clear(const char *key)
{
    assert(key != NULL);

    keydata::pointer keys = begin();

    while(is(keys)) {
        if(eq_case(key, keys->id)) {
            keys->delist(&index);
            return;
        }
        keys.next();
    }
}

void keydata::set(const char *key, const char *value)
{
    assert(key != NULL);

    void *mem = root->alloc(sizeof(keydata::keyvalue));
    keydata::pointer keys = begin();

    while(is(keys)) {
        if(eq_case(key, keys->id)) {
            keys->delist(&index);
            break;
        }
        keys.next();
    }
    new(mem) keydata::keyvalue(root, this, key, value);
}


keyfile::keyfile(size_t pagesize) :
memalloc(pagesize), index()
{
    errcode = 0;
    defaults = NULL;
}

keyfile::keyfile(const char *path, size_t pagesize) :
memalloc(pagesize), index()
{
    errcode = 0;
    defaults = NULL;
    load(path);
}

keyfile::keyfile(const keyfile& copy, size_t pagesize) :
memalloc(pagesize), index()
{
    errcode = 0;
    defaults = NULL;
    load(&copy);
}

void keyfile::assign(keyfile& source)
{
    errcode = source.errcode;
    defaults = source.defaults;
    index.copy(source.index);

    memalloc::assign(source);
    source.errcode = 0;
    source.defaults = NULL;
    source.index.reset();
}

void keyfile::release(void)
{
    defaults = NULL;
    index.reset();
    memalloc::purge();
}

keydata *keyfile::get(const char *key) const
{
    assert(key != NULL);

    keyfile::pointer keys = begin();

    while(is(keys)) {
        if(eq_case(key, keys->name))
            return *keys;
        keys.next();
    }
    return NULL;
}

keydata *keyfile::create(const char *id)
{
    assert(id != NULL);

    void *mem = alloc(sizeof(keydata));
    keydata *old = get(id);

    if(old)
        old->delist(&index);

    return new(mem) keydata(this, id);
}

#ifdef _MSWINDOWS_

bool keyfile::save(HKEY keys, keydata *section, const char *path)
{
    HKEY subkey;

    if(path) {
        if(RegCreateKeyEx(keys, path, 0L, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &subkey, NULL) == ERROR_SUCCESS) {
            save(subkey, section);
            RegCloseKey(subkey);
        }
        else
            errcode = EBADF;
        return false;
    }

    if(!section) {
        if(defaults)
            save(keys, defaults);
        linked_pointer<keydata> kp = begin();
        while(is(kp)) {
            if(RegCreateKeyEx(keys, kp->get(), 0L, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &subkey, NULL) == ERROR_SUCCESS) {
                save(subkey, *kp);
                RegCloseKey(subkey);
            }
            kp.next();
        }
    } else {
        linked_pointer<keydata::keyvalue> kv = section->begin();
        while(is(kv)) {
            const char *value = kv->value;
            RegSetValueEx(keys, kv->id, 0L, REG_SZ, (const BYTE *)value, (DWORD)strlen(value) + 1);
        }
    }
    return true;
}

void keyfile::load(HKEY keys, keydata *section, const char *path)
{
    DWORD index = 0;
    TCHAR keyvalue[256];
    TCHAR keyname[4096];
    DWORD ksize = sizeof(keyname);
    DWORD vsize, vtype;
    FILETIME fTime;
    HKEY subkey;

    if(path) {
        if(RegOpenKeyEx(keys, path, 0, KEY_READ, &subkey) == ERROR_SUCCESS) {
            load(subkey, section);
            RegCloseKey(subkey);
        }
        else
            errcode = EBADF;
        return;
    }

    while(!section && RegEnumKeyEx(keys, index++, keyname, &ksize, NULL, NULL, NULL, &fTime) == ERROR_SUCCESS) {
        if(RegOpenKeyEx(keys, keyname, 0, KEY_READ, &subkey) == ERROR_SUCCESS) {
            section = create(keyname);
            load(subkey, section);
            RegCloseKey(subkey);
        }
        ksize = sizeof(keyname);
    }
    index = 0;
    vsize = sizeof(keyvalue);
    if(vsize > (DWORD)(size() - 64))
        vsize = (DWORD)(size() - 64);
    while((RegEnumValue(keys, index++, keyname, &ksize, NULL, &vtype, (BYTE *)keyvalue, &vsize) == ERROR_SUCCESS) && (vtype == REG_SZ) && (keyname[0] != 0)) {
        if(section)
            section->set(keyname, keyvalue);
        else
            defaults->set(keyname, keyvalue);
        ksize = sizeof(keyname);
        vsize = sizeof(keyvalue);
        if(vsize > (DWORD)(size() - 64))
            vsize = (DWORD)(size() - 64);
    }
}

#endif

void keyfile::load(const keydata *copy)
{
    keydata *section = get(copy->get());
    if(!section)
        section = create(copy->get());

    linked_pointer<keydata::keyvalue> vp = copy->begin();
    while(is(vp)) {
        section->set(vp->id, vp->value);
        vp.next();
    }
}

void keyfile::load(const keyfile *copy)
{
    linked_pointer<keydata::keyvalue> vp = (keydata::keyvalue*)NULL;

    if(copy->defaults)
        vp = copy->defaults->begin();

    if(copy->defaults && !defaults) {
        void *mem = alloc(sizeof(keydata));
        defaults = new(mem) keydata(this);
    }

    while(is(vp)) {
        defaults->set(vp->id, vp->value);
        vp.next();
    }

    keydata *section;
    linked_pointer<keydata> kp = copy->begin();
    while(is(kp)) {
        vp = kp->begin();
        section = get(kp->get());
        if(!section)
            section = create(kp->get());
        while(section && is(vp)) {
            section->set(vp->id, vp->value);
            vp.next();
        }
        kp.next();
    }
}

bool keyfile::save(const char *path)
{
    assert(path != NULL);

    if(!path[0])
        return false;

#ifdef  _MSWINDOWS_
    if(eq(path, "~\\", 2))
        return save(HKEY_CURRENT_USER, NULL, path);
    else if(eq(path, "-\\", 2))
        return save(HKEY_LOCAL_MACHINE, NULL, path);
#endif

    FILE *fp = fopen(path, "w");
    if(!fp) {
        errcode = EBADF;
        return false;
    }

    linked_pointer<keydata::keyvalue> vp = (keydata::keyvalue*)NULL;

    if(defaults)
        vp = defaults->begin();

    while(is(vp)) {
        if(strchr(vp->value, '\"'))
            fprintf(fp, "%s=%s\n", vp->id, vp->value);
        else
            fprintf(fp, "%s=\"%s\"\n", vp->id, vp->value);
        vp.next();
    }
    fprintf(fp, "\n");

    linked_pointer<keydata> kp = begin();
    while(is(kp)) {
        fprintf(fp, "[%s]\n", kp->get());
        vp = kp->begin();
        while(is(vp)) {
            if(strchr(vp->value, '\"'))
                fprintf(fp, "%s=%s\n", vp->id, vp->value);
            else
                fprintf(fp, "%s=\"%s\"\n", vp->id, vp->value);
            vp.next();
        }
        fprintf(fp, "\n");
        kp.next();
    }

    fclose(fp);
    return true;
}

void keyfile::load(const char *path)
{
    assert(path != NULL);

    if(!path[0])
        return;

#ifdef  _MSWINDOWS_
    if(eq(path, "~\\", 2)) {
        load(HKEY_CURRENT_USER, NULL, path);
        return;
    }
    else if(eq(path, "-\\", 2)) {
        load(HKEY_LOCAL_MACHINE, NULL, path);
        return;
    }
#endif

    char linebuf[1024];
    char *lp = linebuf;
    char *ep;
    size_t size = sizeof(linebuf);
    FILE *fp = fopen(path, "r");
    keydata *section = NULL;
    const char *key;
    char *value;

    errcode = 0;

    if(!fp) {
        errcode = EBADF;
        return;
    }

    if(!defaults) {
        void *mem = alloc(sizeof(keydata));
        defaults = new(mem) keydata(this);
    }

    for(;;) {
        *lp = 0;
        if(NULL == fgets(lp, (socksize_t)size, fp)) {
            errcode = ferror(fp);
            lp[0] = 0;
        }
        else
            String::chop(lp, "\r\n\t ");
        ep = lp + strlen(lp);
        if(ep != lp) {
            --ep;
            if(*ep == '\\') {
                lp = ep;
                size = (linebuf + sizeof(linebuf) - ep);
                continue;
            }
        }
        if(!linebuf[0] && feof(fp))
            break;

        lp = linebuf;
        while(isspace(*lp))
            ++lp;

        if(!*lp)
            goto next;

        if(*lp == '[') {
            ep = strchr(lp, ']');
            if(!ep)
                goto next;
            *ep = 0;
            lp = String::strip(++lp, " \t");
            section = get(lp);
            if (!section)
                section = create(lp);
            goto next;
        }
        else if(!isalnum(*lp) || !strchr(lp, '='))
            goto next;

        ep = strchr(lp, '=');
        *ep = 0;
        key = String::strip(lp, " \t");
        value = String::strip(++ep, " \t\r\n");
        value = String::unquote(value, "\"\"\'\'{}()");
        if(section)
            section->set(key, value);
        else
            defaults->set(key, value);
next:
        lp = linebuf;
        size = sizeof(linebuf);
    }
    fclose(fp);
}

} // namespace ucommon
