/*
 * vim:tw=80:expandtab
 *
 * Copyright (c) 2014 The MacPorts Project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#if HAVE_CONFIG_H
#include <config.h>
#endif

#include <sqlite3.h>
#include <stdlib.h>
#include <string.h>
#include <tcl.h>

#include <cregistry/portgroup.h>
#include <cregistry/util.h>

#include "portgroup.h"
#include "portgroupobj.h"
#include "registry.h"
#include "util.h"

/**
 * Converts a command name into a `reg_portgroup`.
 *
 * @param [in] interp  Tcl interpreter to check within
 * @param [in] name    name of portgroup to get
 * @param [out] errPtr description of error if the portgroup can't be found
 * @return             a portgroup, or NULL if one couldn't be found
 * @see get_object
 */
static reg_portgroup* get_portgroup(Tcl_Interp* interp, char* name, reg_error* errPtr) {
    return (reg_portgroup*)get_object(interp, name, "portgroup", portgroup_obj_cmd, errPtr);
}

/**
 * Removes the portgroup from the Tcl interpreter. Doesn't actually delete it since
 * that's the registry's job. This is written to be used as the
 * `Tcl_CmdDeleteProc` for an portgroup object command.
 *
 * @param [in] clientData address of a reg_portgroup to remove
 */
void delete_portgroup(ClientData clientData) {
    reg_portgroup* portgroup = (reg_portgroup*)clientData;
    free(portgroup->proc);
    portgroup->proc = NULL;
}

/**
 * registry::portgroup open id name version size sha256
 *
 * Opens a portgroup matching the given parameters.
 */
static int portgroup_open(Tcl_Interp* interp, int objc, Tcl_Obj* const objv[]) {
	reg_registry* reg = registry_for(interp, reg_attached);
	if (objc != 7) {
		Tcl_WrongNumArgs(interp, 1, objv, "open id name version size sha256");
		return TCL_ERROR;
	} else if (reg == NULL) {
		return TCL_ERROR;
	} else {
		char* id = Tcl_GetString(objv[2]);
		char* name = Tcl_GetString(objv[3]);
        char* version = Tcl_GetString(objv[4]);
        char* size = Tcl_GetString(objv[5]);
        char* sha256 = Tcl_GetString(objv[6]);

		reg_error error;
		reg_portgroup* portgroup = reg_portgroup_open(reg, id, name, version, size, sha256, &error);
		if (portgroup != NULL) {
			Tcl_Obj* result;
			if (portgroup_to_obj(interp, &result, portgroup, NULL, &error)) {
				Tcl_SetObjResult(interp, result);
				return TCL_OK;
			}
		}
		return registry_failed(interp, &error);
	}
	return TCL_ERROR;
}

/**
 * registry::portgroup close portgroup
 *
 * Closes a portgroup. It will remain in the registry.
 */
static int portgroup_close(Tcl_Interp* interp, int objc, Tcl_Obj* const objv[]) {
	if (objc != 3) {
		Tcl_WrongNumArgs(interp, 1, objv, "close portgroup");
		return TCL_ERROR;
	} else {
		reg_error error;
		char* proc = Tcl_GetString(objv[2]);
		reg_portgroup* portgroup = get_portgroup(interp, proc, &error);
		if (portgroup == NULL) {
			return registry_failed(interp, &error);
		} else {
			Tcl_DeleteCommand(interp, proc);
			return TCL_OK;
		}
	}
}

typedef struct {
    char* name;
    reg_strategy strategy;
} strategy_type;

static strategy_type strategies[] = {
    { "-exact",  reg_strategy_exact },
    { "-glob",   reg_strategy_glob },
    { "-regexp", reg_strategy_regexp },
    { "-null",   reg_strategy_null },
    { "--",      reg_strategy_exact },
    { NULL, 0 }
};

/*
 * registry::portgroup search ?key value ...?
 *
 * Searches the registry for portgroups for which each key's value is equal to the
 * given value. To find all portgroups, call `portgroup search` with no key-value pairs.
 * For each key, can be given an option of -exact, -glob, -regexp or -null to
 * specify the matching strategy; defaults to exact.
 */
static int portgroup_search(Tcl_Interp* interp, int objc, Tcl_Obj* const objv[]) {
    int i, j;
    reg_registry* reg = registry_for(interp, reg_attached);
    if (reg == NULL) {
        return TCL_ERROR;
    } else {
        char** keys;
        char** vals;
        int* strats;
        int key_count = 0;
        reg_portgroup** portgroups;
        reg_error error;
        int portgroup_count;
        for (i = 2; i < objc;) {
            int index, strat_index, val_length;
            if (Tcl_GetIndexFromObj(interp, objv[i], portgroup_props, "search key",
                        0, &index) != TCL_OK) {
                return TCL_ERROR;
            }

            /* we ate the key value */
            i++;

            /* check whether there's a strategy */
            if (Tcl_GetString(objv[i])[0] == '-'
                    && Tcl_GetIndexFromObjStruct(interp, objv[i], strategies,
                        sizeof(strategy_type), "option", 0, &strat_index)
                    != TCL_ERROR) {
                /* this key has a strategy specified, eat the strategy parameter */
                i++;

                if (strategies[strat_index].strategy != reg_strategy_null) {
                    /* this key must also have a value */

                    if (Tcl_GetStringFromObj(objv[i], &val_length) == NULL
                            || val_length == 0) {
                        Tcl_WrongNumArgs(interp, 2, objv,
                                "search ?key ?options? value ...?");
                        return TCL_ERROR;
                    }

                    i++;
                }
            } else {
                /* this key must also have a value */

                if (Tcl_GetStringFromObj(objv[i], &val_length) == NULL
                        || val_length == 0) {
                    Tcl_WrongNumArgs(interp, 2, objv,
                            "search ?key ?options? value ...?");
                    return TCL_ERROR;
                }

                i++;
            }

            key_count++;
        }

        keys = malloc(key_count * sizeof(char*));
        vals = malloc(key_count * sizeof(char*));
        strats = malloc(key_count * sizeof(int));
        if (!keys || !vals || !strats) {
            if (keys) {
                free(keys);
            }
            if (vals) {
                free(vals);
            }
            if (strats) {
                free(strats);
            }
            return TCL_ERROR;
        }
        for (i = 2, j = 0; i < objc && j < key_count; j++) {
            int strat_index;

            keys[j] = Tcl_GetString(objv[i++]);

            /* try to get the strategy */
            if (Tcl_GetString(objv[i])[0] == '-'
                    && Tcl_GetIndexFromObjStruct(interp, objv[i], strategies,
                        sizeof(strategy_type), "option", 0, &strat_index)
                    != TCL_ERROR) {
                /* this key has a strategy specified */
                i++;

                strats[j] = strategies[strat_index].strategy;
            } else {
                /* use default strategy */
                strats[j] = reg_strategy_exact;
            }

            if (strats[j] != reg_strategy_null) {
                vals[j] = Tcl_GetString(objv[i++]);
            } else {
                vals[j] = NULL;
            }
        }
        portgroup_count = reg_portgroup_search(reg, keys, vals, strats, key_count,
                &portgroups, &error);
        free(keys);
        free(vals);
        free(strats);
        if (portgroup_count >= 0) {
            int retval;
            Tcl_Obj* resultObj;
            Tcl_Obj** objs;
            if (list_portgroup_to_obj(interp, &objs, portgroups, portgroup_count, &error)){
                resultObj = Tcl_NewListObj(portgroup_count, objs);
                Tcl_SetObjResult(interp, resultObj);
                free(objs);
                retval = TCL_OK;
            } else {
                retval = registry_failed(interp, &error);
            }
            free(portgroups);
            return retval;
        }
        return registry_failed(interp, &error);
    }
}

typedef struct {
    char* name;
    int (*function)(Tcl_Interp* interp, int objc, Tcl_Obj* const objv[]);
} portgroup_cmd_type;

static portgroup_cmd_type portgroup_cmds[] = {
    /* Global commands */
    { "open", portgroup_open },
    { "close", portgroup_close },
    { "search", portgroup_search },
    { NULL, NULL }
};

/*
 * registry::portgroup cmd ?arg ...?
 *
 * Commands manipulating portgroup entries in the registry. This can be called `registry::portgroup`
 */
int portgroup_cmd(ClientData clientData UNUSED, Tcl_Interp* interp, int objc,
        Tcl_Obj* const objv[]) {
    int cmd_index;
    if (objc < 2) {
        Tcl_WrongNumArgs(interp, 1, objv, "cmd ?arg ...?");
        return TCL_ERROR;
    }
    if (Tcl_GetIndexFromObjStruct(interp, objv[1], portgroup_cmds,
                sizeof(portgroup_cmd_type), "cmd", 0, &cmd_index) == TCL_OK) {
        portgroup_cmd_type* cmd = &portgroup_cmds[cmd_index];
        return cmd->function(interp, objc, objv);
    }
    return TCL_ERROR;
}
