/** CSS module management
 * @file */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdlib.h>

#include "elinks.h"

#include "cache/cache.h"
#include "config/home.h"
#include "config/options.h"
#include "document/css/css.h"
#include "document/css/parser.h"
#include "document/css/stylesheet.h"
#include "encoding/encoding.h"
#include "intl/libintl.h"
#include "main/module.h"
#include "network/connection.h"
#include "protocol/uri.h"
#include "session/session.h"
#include "util/error.h"
#include "util/memory.h"
#include "viewer/text/draw.h"


union option_info css_options_info[] = {
	INIT_OPT_TREE("document", N_("Cascading Style Sheets"),
		"css", OPT_SORT,
		N_("Options concerning how to use CSS for styling "
		"documents.")),

	INIT_OPT_BOOL("document.css", N_("Enable CSS"),
		"enable", OPT_ZERO, 1,
		N_("Enable adding of CSS style info to documents.")),
#ifdef CONFIG_LIBCSS
	INIT_OPT_BOOL("document.css", N_("Prefer libcss"),
		"libcss", OPT_ZERO, 0,
		N_("Enable experimental code using the libcss library instead of builtin implementation.")),
#endif
	INIT_OPT_BOOL("document.css", N_("Ignore \"display: none\""),
		"ignore_display_none", OPT_ZERO, 1,
		N_("When enabled, elements are rendered, even when their "
		"display property has the value \"none\". Because ELinks's "
		"CSS support is still very incomplete, this setting can "
		"improve the way that some documents are rendered.")),

	INIT_OPT_BOOL("document.css", N_("Import external style sheets"),
		"import", OPT_ZERO, 1,
		N_("When enabled any external style sheets that are imported "
		"from either CSS itself using the @import keyword or from the "
		"HTML using <link> tags in the document header will also be "
		"downloaded.")),

	INIT_OPT_STRING("document.css", N_("Default style sheet"),
		"stylesheet", OPT_ZERO, "",
		N_("The path to the file containing the default user defined "
		"Cascading Style Sheet. It can be used to control the basic "
		"layout of HTML documents. The path is assumed to be relative "
		"to ELinks' home directory.\n"
		"\n"
		"Leave as \"\" to use built-in document styling.")),

	INIT_OPT_STRING("document.css", N_("Media types"),
		"media", OPT_ZERO, "tty",
		N_("CSS media types that ELinks claims to support, separated "
		"with commas. The \"all\" type is implied. Currently, only "
		"ASCII characters work reliably here.  See CSS2 section 7: "
		"http://www.w3.org/TR/1998/REC-CSS2-19980512/media.html")),

	NULL_OPTION_INFO,
};


/** Check whether ELinks claims to support a specific CSS media type.
 *
 * @param optstr
 *   Null-terminated value of the document.css.media option.
 * @param token
 *   A name parsed from a CSS file or from an HTML media attribute.
 *   Need not be null-terminated.
 * @param token_length
 *   Length of @a token, in bytes.
 *
 * Both strings should be in the ASCII charset.
 *
 * @return nonzero if the media type is supported, 0 if not.  */
int
supports_css_media_type(const char *optstr,
			const char *token, size_t token_length)
{
	ELOG
	/* Split @optstr into comma-delimited strings, strip leading
	 * and trailing spaces from each, and compare them to the
	 * token.  */
	while (*optstr != '\0') {
		const char *beg, *end;

		while (*optstr == ' ')
			++optstr;

		beg = optstr;
		optstr += strcspn(optstr, ",");
		end = optstr;
		while (end > beg && end[-1] == ' ')
			--end;

		/* W3C REC-html401-19991224 section 6.13:
		 *   "3. A case-sensitive match is then made with
		 *    the set of media types defined above."
		 * W3C REC-html401-19991224 section 14.2.3:
		 *   "media = media-descriptors [CI]"
		 *   where CI stands for case-insensitive.
		 * This mismatch has been reported to the
		 * www-html-editor@w3.org mailing list on 2000-11-16.
		 *
		 * W3C REC-CSS2-19980512 section 7.3:
		 *  "Media type names are case-insensitive."  */
		if (!strlcasecmp(token, token_length, beg, end - beg))
			return 1;

		while (*optstr == ',')
			++optstr;
	}

	/* An explicit "all" is probably rarer than e.g. "tty".  */
	if (!strlcasecmp(token, token_length, "all", 3))
		return 1;

	return 0;
}

void
import_css(struct css_stylesheet *css, struct uri *uri)
{
	ELOG
	/* Do we have it in the cache? (TODO: CSS cache) */
	struct cache_entry *cached;
	struct fragment *fragment;

	if (!uri || css->import_level >= MAX_REDIRECTS)
		return;

	cached = get_redirected_cache_entry(uri);
	if (!cached) return;

	fragment = get_cache_fragment(cached);
	if (fragment) {
		char *end = fragment->data + fragment->length;

		css->import_level++;
		css_parse_stylesheet(css, uri, fragment->data, end);
		css->import_level--;
	}
}


static void
import_css_file(struct css_stylesheet *css, struct uri *base_uri,
		const char *url, int urllen)
{
	ELOG
	char *xdg_config_home = get_xdg_config_home();
	struct string string, filename;

	if (!*url
	    || css->import_level >= MAX_REDIRECTS
	    || !init_string(&filename))
		return;

	if (*url != '/' && xdg_config_home) {
		add_to_string(&filename, xdg_config_home);
	}

	add_bytes_to_string(&filename, url, urllen);

	if (is_in_state(read_encoded_file(&filename, &string), S_OK)) {
		char *end = string.source + string.length;

		css->import_level++;
		css_parse_stylesheet(css, base_uri, string.source, end);
		done_string(&string);
		css->import_level--;
	}

	done_string(&filename);
}

struct css_stylesheet default_stylesheet = INIT_CSS_STYLESHEET(default_stylesheet, import_css_file);

static void
import_default_css(void)
{
	ELOG
	char *url = get_opt_str("document.css.stylesheet", NULL);

	if (!css_selector_set_empty(&default_stylesheet.selectors))
		done_css_stylesheet(&default_stylesheet);

	if (!*url) return;

	import_css_file(&default_stylesheet, NULL, url, strlen(url));
}

static int
change_hook_css(struct session *ses, struct option *current, struct option *changed)
{
	ELOG
	int reload_css = 0;

	if (!strcmp(changed->name, "stylesheet")) {
		/** @todo TODO: We need to update all entries in
		 * format cache. --jonas */
		import_default_css();
	}

	if (!strcmp(changed->name, "media"))
		reload_css = 1;

	/* Instead of using the value of the @ses parameter, iterate
	 * through the @sessions list.  The parameter may be NULL and
	 * anyway we don't support session-specific options yet.  */
	foreach (ses, sessions) draw_formatted(ses, 1 + reload_css);

	return 0;
}

static void
init_css(struct module *module)
{
	ELOG
	static const struct change_hook_info css_change_hooks[] = {
		{ "document.css",		change_hook_css },
		{ NULL,				NULL },
	};

	register_change_hooks(css_change_hooks);
	import_default_css();
}

void
done_css(struct module *module)
{
	ELOG
	done_css_stylesheet(&default_stylesheet);
}


struct module css_module = struct_module(
	/* name: */		N_("Cascading Style Sheets"),
	/* options: */		css_options_info,
	/* hooks: */		NULL,
	/* submodules: */	NULL,
	/* data: */		NULL,
	/* init: */		init_css,
	/* done: */		done_css,
	/* getname: */	NULL
);
