/* config-ini.c -- INI config file parser
   This file is part of Redshift.

   Redshift 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 3 of the License, or
   (at your option) any later version.

   Redshift 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 Redshift.  If not, see <http://www.gnu.org/licenses/>.

   Copyright (c) 2010  Jon Lund Steffensen <jonlst@gmail.com>
*/


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#ifndef _WIN32
# include <pwd.h>
#endif

#include "config-ini.h"

#ifdef ENABLE_NLS
# include <libintl.h>
# define _(s) gettext(s)
#else
# define _(s) s
#endif

#define MAX_CONFIG_PATH  4096
#define MAX_LINE_LENGTH   512


static FILE *
open_config_file(const char *filepath)
{
	FILE *f = NULL;

	/* If a path is not specified (filepath is NULL) then
	   the configuration file is searched for in the directories
	   specified by the XDG Base Directory Specification
	   <http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html>.

	   If HOME is not set, getpwuid() is consulted for the home directory. On
	   windows platforms the %localappdata% is used in place of XDG_CONFIG_HOME.
	*/

	if (filepath == NULL) {
		FILE *f = NULL;
		char cp[MAX_CONFIG_PATH];
		char *env;

		if (f == NULL && (env = getenv("XDG_CONFIG_HOME")) != NULL &&
		    env[0] != '\0') {
			snprintf(cp, sizeof(cp), "%s/redshift.conf", env);
			f = fopen(cp, "r");
		}

#ifdef _WIN32
		if (f == NULL && (env = getenv("localappdata")) != NULL &&
		    env[0] != '\0') {
			snprintf(cp, sizeof(cp),
				 "%s\\redshift.conf", env);
			f = fopen(cp, "r");
		}
#endif
		if (f == NULL && (env = getenv("HOME")) != NULL &&
		    env[0] != '\0') {
			snprintf(cp, sizeof(cp),
				 "%s/.config/redshift.conf", env);
			f = fopen(cp, "r");
		}
#ifndef _WIN32

		if (f == NULL) {
			struct passwd *pwd = getpwuid(getuid());
			char *home = pwd->pw_dir;
			snprintf(cp, sizeof(cp),
				 "%s/.config/redshift.conf", home);
			f = fopen(cp, "r");
		}

		if (f == NULL && (env = getenv("XDG_CONFIG_DIRS")) != NULL &&
		    env[0] != '\0') {
			char *begin = env;
			while (1) {
				char *end = strchr(begin, ':');
				if (end == NULL) end = strchr(begin, '\0');

				int len = end - begin;
				if (len > 0) {
					snprintf(cp, sizeof(cp),
						 "%.*s/redshift.conf", len, begin);

					f = fopen(cp, "r");
					if (f != NULL) break;
				}

				if (end[0] == '\0') break;
				begin = end + 1;
			}
		}

		if (f == NULL) {
			snprintf(cp, sizeof(cp),
				 "%s/redshift.conf", "/etc");
			f = fopen(cp, "r");
		}
#endif

		return f;
	} else {
		f = fopen(filepath, "r");
		if (f == NULL) {
			perror("fopen");
			return NULL;
		}
	}

	return f;
}

int
config_ini_init(config_ini_state_t *state, const char *filepath)
{
	config_ini_section_t *section = NULL;
	state->sections = NULL;

	FILE *f = open_config_file(filepath);
	if (f == NULL) {
		/* Only a serious error if a file was explicitly requested. */
		if (filepath != NULL) return -1;
		return 0;
	}

	char line[MAX_LINE_LENGTH];
	char *s;

	while (1) {
		/* Handle the file input linewise. */
		char *r = fgets(line, sizeof(line), f);
		if (r == NULL) break;

		/* Strip leading blanks and trailing newline. */
		s = line + strspn(line, " \t");
		s[strcspn(s, "\r\n")] = '\0';

		/* Skip comments and empty lines. */
		if (s[0] == ';' || s[0] == '\0') continue;

		if (s[0] == '[') {
			/* Read name of section. */
			const char *name = s+1;
			char *end = strchr(s, ']');
			if (end == NULL || end[1] != '\0' || end == name) {
				fputs(_("Malformed section header in config"
					" file.\n"), stderr);
				fclose(f);
				config_ini_free(state);
				return -1;
			}

			*end = '\0';

			/* Create section. */
			section = malloc(sizeof(config_ini_section_t));
			if (section == NULL) {
				fclose(f);
				config_ini_free(state);
				return -1;
			}

			/* Insert into section list. */
			section->name = NULL;
			section->settings = NULL;
			section->next = state->sections;
			state->sections = section;

			/* Copy section name. */
			section->name = malloc(end - name + 1);
			if (section->name == NULL) {
				fclose(f);
				config_ini_free(state);
				return -1;
			}

			memcpy(section->name, name, end - name + 1);
		} else {
			/* Split assignment at equals character. */
			char *end = strchr(s, '=');
			if (end == NULL || end == s) {
				fputs(_("Malformed assignment in config"
					" file.\n"), stderr);
				fclose(f);
				config_ini_free(state);
				return -1;
			}

			*end = '\0';
			char *value = end + 1;

			if (section == NULL) {
				fputs(_("Assignment outside section in config"
					" file.\n"), stderr);
				fclose(f);
				config_ini_free(state);
				return -1;
			}

			/* Create section. */
			config_ini_setting_t *setting =
				malloc(sizeof(config_ini_setting_t));
			if (setting == NULL) {
				fclose(f);
				config_ini_free(state);
				return -1;
			}

			/* Insert into section list. */
			setting->name = NULL;
			setting->value = NULL;
			setting->next = section->settings;
			section->settings = setting;

			/* Copy name of setting. */
			setting->name = malloc(end - s + 1);
			if (setting->name == NULL) {
				fclose(f);
				config_ini_free(state);
				return -1;
			}

			memcpy(setting->name, s, end - s + 1);

			/* Copy setting value. */
			size_t value_len = strlen(value) + 1;
			setting->value = malloc(value_len);
			if (setting->value == NULL) {
				fclose(f);
				config_ini_free(state);
				return -1;
			}

			memcpy(setting->value, value, value_len);
		}
	}

	fclose(f);

	return 0;
}

void
config_ini_free(config_ini_state_t *state)
{
	config_ini_section_t *section = state->sections;

	while (section != NULL) {
		config_ini_setting_t *setting = section->settings;
		config_ini_section_t *section_prev = section;

		while (setting != NULL) {
			config_ini_setting_t *setting_prev = setting;
			free(setting->name);
			free(setting->value);
			setting = setting->next;
			free(setting_prev);
		}

		free(section->name);
		section = section->next;
		free(section_prev);
	}
}

config_ini_section_t *
config_ini_get_section(config_ini_state_t *state, const char *name)
{
	config_ini_section_t *section = state->sections;
	while (section != NULL) {
		if (strcasecmp(section->name, name) == 0) {
			return section;
		}
		section = section->next;
	}

	return NULL;
}