/*************************************************************************/
/* Copyright (C) 2019-2020 Amir Plivatsky                                     */
/* All rights reserved                                                   */
/*                                                                       */
/* Use of the link grammar parsing system is subject to the terms of the */
/* license set forth in the LICENSE file included with this software.    */
/* This license allows free redistribution and use in source and binary  */
/* forms, with or without modification, subject to certain conditions.   */
/*                                                                       */
/*************************************************************************/

#include <string.h>

#include "api-structures.h"
#include "dialect.h"
#include "dict-api.h"                   // cost_stringify
#include "dict-common.h"
#include "dict-structures.h"
#include "dict-file/read-dialect.h"     // dialect_read_from_one_line_str
#include "file-utils.h"
#include "string-id.h"
#include "string-set.h"

#define D_DIALECT 7 /* Verbosity level for this file (also 8). */

static void free_dialect_table(Dialect *di)
{
	free(di->table);
	free(di->kept_input);
}

void free_dialect(Dialect *di)
{
	if (di == NULL) return;
	free_dialect_table(di);
	free(di->section);
	string_id_delete(di->section_set);
	free(di);
}

Dialect *dialect_alloc(void)
{
	Dialect *di = malloc(sizeof(*di));
	memset(di, 0, sizeof(*di));
	di->section_set = string_id_create();
	return di;
}

unsigned int exptag_dialect_add(Dictionary dict, const char *tag)
{
	expression_tag *dt = &dict->dialect_tag;
	unsigned int tag_index = string_id_lookup(tag, dt->set);

	if (tag_index != SID_NOTFOUND) return tag_index;
	tag_index = string_id_add(tag, dt->set);
	tag = string_set_add(tag, dict->string_set); /* FIXME: Refer to string-id */

	if (dt->num == dt->size)
	{
		if (dt->num == 0)
			dt->size = EXPTAG_SZ;
		else
			dt->size *= 2;

		dt->name = realloc(dt->name, dt->size * sizeof(*dt->name));
	}
	dt->name[tag_index] = tag;
	dt->num++;
	assert(dt->num == tag_index, "Tag index mismatch");

	return tag_index;
}

/**
 * Set in the cost table the dialect component cost at \p table_index.
 * @return \c false iff the component doesn't exist in the dictionary.
 */
static bool apply_component(Dictionary dict, Dialect *di,
                            unsigned int table_index, float *cost_table)
{
	expression_tag *dt = &dict->dialect_tag;
	unsigned int cost_index =
		string_id_lookup(di->table[table_index].name, dt->set);

	if (cost_index == SID_NOTFOUND)
	{
		prt_error("Error: Dialect component \"%s\" is not in the dictionary.\n",
			di->table[table_index].name);
		return false;
	}
	cost_table[cost_index] = di->table[table_index].cost;

	return true;
}

/**
 * Apply the dialect settings at \p from to the dialect table index
 * \p table_index at \p to.
 * The dialect table at \p from has been created from the "4.0.dialect"
 * file or from the user setting kept in the dialect_info option.
 * The dialect table at \p to is always from "4.0.dialect".
 * Recursive (for applying sub-dialects).
 * @return \c true on success, \c false on failure.
 */
static bool apply_table_entry(Dictionary dict, Dialect *from,
                              unsigned int table_index, Dialect *to,
                              dialect_info *dinfo, bool *encountered)
{
	int skip = (int)(to == from); /* Skip an initial section header. */

	/* Apply everything until the next section (or "from" table end). */
	for (unsigned int i = table_index + skip; i < from->num_table_tags; i++)
	{
		if (cost_eq(from->table[i].cost, DIALECT_SECTION)) break;

		/* Apply components and sub-dialects. */
		lgdebug(+D_DIALECT, "Apply %s %s%s\n",
		        from->table[i].name, cost_stringify(from->table[i].cost),
		        (to == from) ? "" : " (user setup)");
		if (!cost_eq(from->table[i].cost, DIALECT_SUB))
		{
			if (!apply_component(dict, from, i, dinfo->cost_table)) return false;
		}
		else
		{
			unsigned int sub_index = SID_NOTFOUND;
			if (to != NULL)
				sub_index = string_id_lookup(from->table[i].name, to->section_set);
			if (sub_index == SID_NOTFOUND)
			{
				prt_error("Error: Undefined dialect \"%s\"\n", from->table[i].name);
				return false;
			}
			if (encountered[sub_index])
			{
				prt_error("Error: Loop detected at sub-dialect \"%s\" "
				          "(of dialect \"%s\").\n",
				          to->table[i].name, to->table[table_index].name);
				return false;
			}
			encountered[sub_index] = true;
			if (!apply_table_entry(dict, to, to->section[sub_index].index, to,
			                       dinfo, encountered))
				return false;
		}
	}

	return true;
}

bool apply_dialect(Dictionary dict, Dialect *from, unsigned int table_index,
                   Dialect *to, dialect_info *dinfo)
{
	/* Loop detection (needed only if there are "4.0.dialect" definitions). */
	bool *loopdet;
	if (to == NULL)
	{
		loopdet = NULL;
	}
	else
	{
		loopdet = alloca(to->num_sections + 1);
		memset(loopdet, 0, to->num_sections + 1);
	}

	if (!apply_table_entry(dict, from, table_index, to, dinfo, loopdet))
		return false;

	return true;
}

static void print_cost_table(Dictionary dict, Dialect *di, dialect_info *dinfo)
{
	expression_tag *dt = &dict->dialect_tag;

	if (dt->num == 0)
	{
		assert(dinfo->cost_table == NULL, "Unexpected cost table.");
		prt_error("Debug: No dialect cost table (no tags in the dict).\n");
		return;
	}

	if (dinfo->cost_table == NULL)
	{
		prt_error("Debug: No dialect cost table.\n");
		return;
	}

	prt_error("Dialect cost table (%u component%s):\n\\",
	          dt->num, dt->num == 1 ? "" : "s");
	prt_error("%-15s %s\n", "component", "cost");

	for (unsigned int i = 1; i  <= dt->num; i++)
	{
		prt_error("%-15s %s\n\\",
		          dt->name[i], cost_stringify(dinfo->cost_table[i]));
	}
	lg_error_flush();
}

void free_cost_table(Parse_Options opts)
{
	free(opts->dialect.cost_table);
	opts->dialect.cost_table = NULL;
}

static bool dialect_conf_exists(dialect_info *dinfo)
{
	for (const char *p = dinfo->conf; *p != '\0'; p++)
		if (!lg_isspace((unsigned char)*p)) return true;
	return false;
}

const char no_dialect[] = "(unset the dialect option)\n";

/**
 * Build the dialect cost table if it doesn't exist.
 * Note: All IDs start from 1, so the 0'th elements are ignored
 * (but section[0] which points to the default section).
 * @return \c true on success, \c false on failure.
 */
bool setup_dialect(Dictionary dict, Parse_Options opts)
{
	Dialect *di = dict->dialect;
	dialect_info *dinfo = &opts->dialect;
	expression_tag *dt = &dict->dialect_tag;

	if (dt->num == 0)
	{
		if (!dialect_conf_exists(dinfo)) return true;
		prt_error("Error: Dialect setup failed: No dialects in the \"%s\" "
					 "dictionary %s.\n", dict->lang, no_dialect);
		return false;
	}

	if (dinfo->cost_table != NULL)
	{
		/* Cached table. */
		if ((dinfo->dict != dict) || (dict->cached_dialect != dinfo))
		{
			/* XXX It may still be a stale cache if the dictionary got the
			 * same address as a previously-closed one and also "opts" got the
			 * same address as a previously-deleted "opts" (very unlikely).
			 * Can be fixed by adding dictionary_create() ordinal number +
			 * stack address at the time of grabbing the ordinal number:
			 * dict_id = ++static_id; dict_stack_address_stamp = &auto_var; */
			lgdebug(+D_DIALECT,
			        "Debug: Resetting dialect cache of a different dictionary.\n");
			free_cost_table(opts);
		}
		else
		{
			lgdebug(+D_DIALECT, "Debug: Cached cost table found\n");

			if (verbosity_level(+D_DIALECT+1))
				print_cost_table(dict, di, dinfo);

			return true;
		}
	}

	dinfo->dict = dict;
	dict->cached_dialect = dinfo;

	if (dt->num != 0)
	{
		dinfo->cost_table = malloc((dt->num + 1) * sizeof(*dinfo->cost_table));

		for (unsigned int i = 1; i <= dt->num; i++)
			dinfo->cost_table[i] = DIALECT_COST_DISABLE;
	}

	/* Apply the default section of the dialect table, if exists. */
	if ((di != NULL) && (di->section != NULL) &&
	    (di->section[0].index != NO_INDEX))
	{
		if (!apply_dialect(dict, di, di->section[0].index, di, dinfo))
			return false;
	}

	if (dialect_conf_exists(dinfo))
	{
		Dialect user_setup = (Dialect){ 0 };
		if (!dialect_read_from_one_line_str(dict, &user_setup, dinfo->conf))
		{
			free_dialect_table(&user_setup);
			return false;
		}

		if (!apply_dialect(dict, &user_setup, /*table index*/0, di, dinfo))
		{
			free_dialect_table(&user_setup);
			return false;
		}
		free_dialect_table(&user_setup);
	}

	if (verbosity_level(+D_DIALECT+1))
		print_cost_table(dict, di, dinfo);

	return true;
}
