/*
 * Copyright 2004, 2005 Richard Wilson <info@tinct.net>
 *
 * This file is part of NetSurf, http://www.netsurf-browser.org/
 *
 * NetSurf 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; version 2 of the License.
 *
 * NetSurf 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * \file
 * Window themes implementation.
 */

#include <alloca.h>
#include <assert.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include "oslib/dragasprite.h"
#include "oslib/os.h"
#include "oslib/osgbpb.h"
#include "oslib/osfile.h"
#include "oslib/osfind.h"
#include "oslib/osspriteop.h"
#include "oslib/wimpspriteop.h"
#include "oslib/squash.h"
#include "oslib/wimp.h"
#include "oslib/wimpextend.h"
#include "oslib/wimpspriteop.h"

#include "utils/nsoption.h"
#include "utils/log.h"

#include "riscos/cookies.h"
#include "riscos/dialog.h"
#include "riscos/global_history.h"
#include "riscos/gui.h"
#include "riscos/hotlist.h"
#include "riscos/menus.h"
#include "riscos/theme.h"
#include "riscos/toolbar.h"
#include "riscos/wimp.h"
#include "riscos/wimp_event.h"
#include "riscos/wimputils.h"

/** @todo provide a proper interface for these and make them static again! */

static struct theme_descriptor *theme_current = NULL;
static struct theme_descriptor *theme_descriptors = NULL;

static bool ro_gui_theme_add_descriptor(const char *folder, const char *leafname);
static void ro_gui_theme_get_available_in_dir(const char *directory);
static void ro_gui_theme_free(struct theme_descriptor *descriptor);

/**
 * Initialise the theme handler
 */
void ro_gui_theme_initialise(void)
{
	struct theme_descriptor *descriptor;

	theme_descriptors = ro_gui_theme_get_available();
	descriptor = ro_gui_theme_find(nsoption_charp(theme));
	if (!descriptor)
		descriptor = ro_gui_theme_find("Aletheia");
	ro_gui_theme_apply(descriptor);
}


/**
 * Finalise the theme handler
 */
void ro_gui_theme_finalise(void)
{
	ro_gui_theme_close(theme_current, false);
	ro_gui_theme_free(theme_descriptors);
}


/**
 * Finds a theme from the cached values.
 *
 * The returned theme is only guaranteed to be valid until the next call
 * to ro_gui_theme_get_available() unless it has been opened using
 * ro_gui_theme_open().
 *
 * \param leafname  the filename of the theme_descriptor to return
 * \return the requested theme_descriptor, or NULL if not found
 */
struct theme_descriptor *ro_gui_theme_find(const char *leafname)
{
	struct theme_descriptor *descriptor;

	if (!leafname)
		return NULL;

	for (descriptor = theme_descriptors; descriptor;
			descriptor = descriptor->next)
		if (!strcmp(leafname, descriptor->leafname))
			return descriptor;
	/* fallback for 10 chars on old filesystems */
	for (descriptor = theme_descriptors; descriptor;
			descriptor = descriptor->next)
		if (!strncmp(leafname, descriptor->leafname, 10))
			return descriptor;
	return NULL;
}


/**
 * Reads and caches the currently available themes.
 *
 * \return the requested theme_descriptor, or NULL if not found
 */
struct theme_descriptor *ro_gui_theme_get_available(void)
{
	struct theme_descriptor *current;
	struct theme_descriptor *test;

	/* close any unused descriptors */
	ro_gui_theme_free(theme_descriptors);

	/* add our default 'Aletheia' theme */
	ro_gui_theme_add_descriptor("NetSurf:Resources", "Aletheia");

	/* scan our choices directory */
	ro_gui_theme_get_available_in_dir(nsoption_charp(theme_path));

	/* sort alphabetically in a very rubbish way */
	if ((theme_descriptors) && (theme_descriptors->next)) {
		current = theme_descriptors;
		while ((test = current->next)) {
			if (strcmp(current->name, test->name) > 0) {
				current->next->previous = current->previous;
				if (current->previous)
					current->previous->next = current->next;
				current->next = test->next;
				test->next = current;
				current->previous = test;
				if (current->next)
					current->next->previous = current;

				current = test->previous;
				if (!current) current = test;
			} else {
				current = current->next;
			}
		}
		while (theme_descriptors->previous)
			theme_descriptors = theme_descriptors->previous;
	}

	return theme_descriptors;
}


/**
 * Adds the themes in a directory to the global cache.
 *
 * \param directory  the directory to scan
 */
static void ro_gui_theme_get_available_in_dir(const char *directory)
{
	int context = 0;
	int read_count;
	osgbpb_INFO(100) info;

	while (context != -1) {
	  	/* read some directory info */
		os_error *error = xosgbpb_dir_entries_info(directory,
				(osgbpb_info_list *) &info, 1, context,
				sizeof(info), 0, &read_count, &context);
		if (error) {
			NSLOG(netsurf, INFO,
			      "xosgbpb_dir_entries_info: 0x%x: %s",
			      error->errnum,
			      error->errmess);
			if (error->errnum == 0xd6)	/* no such dir */
				return;
			ro_warn_user("MiscError", error->errmess);
			break;
		}

		/* only process files */
		if ((read_count != 0) && (info.obj_type == fileswitch_IS_FILE))
			ro_gui_theme_add_descriptor(directory, info.name);
	}
}


/**
 * Returns the current theme handle, or NULL if none is set.
 *
 * \return		The theme descriptor handle, or NULL.
 */

struct theme_descriptor *ro_gui_theme_get_current(void)
{
	return theme_current;
}


/**
 * Returns a sprite area for use with the given theme.  This may return a
 * pointer to the wimp sprite pool if a theme area isn't available.
 *
 * \param *descriptor		The theme to use, or NULL for the current.
 * \return			A pointer to the theme sprite area.
 */

osspriteop_area *ro_gui_theme_get_sprites(struct theme_descriptor *descriptor)
{
	osspriteop_area		*area;

	if (descriptor == NULL)
		descriptor = theme_current;

	if (descriptor != NULL && descriptor->theme != NULL)
		area = descriptor->theme->sprite_area;
	else
		area = (osspriteop_area *) 1;

	return area;
}


/**
 * Returns an interger element from the specified theme, or the current theme
 * if the descriptor is NULL.
 *
 * This is an attempt to abstract the theme data from its clients: it should
 * simplify the task of expanding the theme system in the future should this
 * be necessary to include other parts of the RISC OS GUI in the theme system.
 *
 * \param *descriptor		The theme to use, or NULL for the current.
 * \param style			The style to use.
 * \param element		The style element to return.
 * \return			The requested value, or 0.
 */

int ro_gui_theme_get_style_element(struct theme_descriptor *descriptor,
		theme_style style, theme_element element)
{
	if (descriptor == NULL)
		descriptor = theme_current;

	if (descriptor == NULL)
		return 0;

	switch (style) {
	case THEME_STYLE_NONE:
		switch(element) {
		case THEME_ELEMENT_FOREGROUND:
			return wimp_COLOUR_BLACK;
		case THEME_ELEMENT_BACKGROUND:
			return wimp_COLOUR_VERY_LIGHT_GREY;
		default:
			return 0;
		}
		break;

	case THEME_STYLE_BROWSER_TOOLBAR:
		switch (element) {
		case THEME_ELEMENT_FOREGROUND:
			return wimp_COLOUR_BLACK;
		case THEME_ELEMENT_BACKGROUND:
			return descriptor->browser_background;
		default:
			return 0;
		}
		break;

	case THEME_STYLE_HOTLIST_TOOLBAR:
	case THEME_STYLE_COOKIES_TOOLBAR:
	case THEME_STYLE_GLOBAL_HISTORY_TOOLBAR:
		switch (element) {
		case THEME_ELEMENT_FOREGROUND:
			return wimp_COLOUR_BLACK;
		case THEME_ELEMENT_BACKGROUND:
			return descriptor->hotlist_background;
		default:
			return 0;
		}
		break;

	case THEME_STYLE_STATUS_BAR:
		switch (element) {
		case THEME_ELEMENT_FOREGROUND:
			return descriptor->status_foreground;
		case THEME_ELEMENT_BACKGROUND:
			return descriptor->status_background;
		default:
			return 0;
		}
		break;

	default:
		return 0;
	}
}

/**
 * Returns details of the throbber as defined in a theme.
 *
 * \param *descriptor		The theme of interest (NULL for current).
 * \param *frames		Return the number of animation frames.
 * \param *width		Return the throbber width.
 * \param *height		Return the throbber height.
 * \param *right		Return the 'locate on right' flag.
 * \param *redraw		Return the 'forcible redraw' flag.
 * \return			true if meaningful data has been returned;
 *				else false.
 */

bool ro_gui_theme_get_throbber_data(struct theme_descriptor *descriptor,
		int *frames, int *width, int *height,
		bool *right, bool *redraw)
{
	if (descriptor == NULL)
		descriptor = theme_current;

	if (descriptor == NULL || descriptor->theme == NULL)
		return false;

	if (frames != NULL)
		*frames = descriptor->theme->throbber_frames;
	if (width != NULL)
		*width = descriptor->theme->throbber_width;
	if (height != NULL)
		*height = descriptor->theme->throbber_height;
	if (right != NULL)
		*right = descriptor->throbber_right;
	if (redraw != NULL)
		*redraw = descriptor->throbber_redraw;

	return true;
}


/**
 * Checks a theme is valid and adds it to the current list
 *
 * \param folder	the theme folder
 * \param leafname	the theme leafname
 * \return whether the theme was added
 */
bool ro_gui_theme_add_descriptor(const char *folder, const char *leafname)
{
	struct theme_file_header file_header;
	struct theme_descriptor *current;
	struct theme_descriptor *test;
	int output_left;
	os_fw file_handle;
	os_error *error;
	char *filename;

	/* create a full filename */
	filename = malloc(strlen(folder) + strlen(leafname) + 2);
	if (!filename) {
	  	NSLOG(netsurf, INFO, "No memory for malloc");
	  	ro_warn_user("NoMemory", 0);
	  	return false;
	}
	sprintf(filename, "%s.%s", folder, leafname);

	/* get the header */
	error = xosfind_openinw(osfind_NO_PATH, filename, 0,
			&file_handle);
	if (error) {
		NSLOG(netsurf, INFO, "xosfind_openinw: 0x%x: %s",
		      error->errnum, error->errmess);
		ro_warn_user("FileError", error->errmess);
		free(filename);
		return false;
	}
	if (file_handle == 0) {
		free(filename);
		return false;
	}
	error = xosgbpb_read_atw(file_handle,
			(byte *) &file_header,
			sizeof (struct theme_file_header),
			0, &output_left);
	xosfind_closew(file_handle);
	if (error) {
		NSLOG(netsurf, INFO, "xosbgpb_read_atw: 0x%x: %s",
		      error->errnum, error->errmess);
		ro_warn_user("FileError", error->errmess);
		free(filename);
		return false;
	}
	if (output_left > 0) {	/* should try to read more? */
	  	free(filename);
	  	return false;
	}

	/* create a new theme descriptor */
	current = (struct theme_descriptor *)calloc(1,
			sizeof(struct theme_descriptor));
	if (!current) {
		NSLOG(netsurf, INFO, "calloc failed");
		ro_warn_user("NoMemory", 0);
		free(filename);
		return false;
	}
	if (!ro_gui_theme_read_file_header(current, &file_header)) {
		free(filename);
		free(current);
		return false;
	}
	current->filename = filename;
	current->leafname = current->filename + strlen(folder) + 1;

	/* don't add duplicates */
	for (test = theme_descriptors; test; test = test->next) {
		if (!strcmp(current->name, test->name)) {
			free(current->filename);
			free(current);
			return false;
		}
	}

	/* link in our new descriptor at the head*/
	if (theme_descriptors) {
		current->next = theme_descriptors;
		theme_descriptors->previous = current;
	}
	theme_descriptors = current;
	return true;

}


/**
 * Fills in the basic details for a descriptor from a file header.
 * The filename string is not set.
 *
 * \param descriptor   the descriptor to set up
 * \param file_header  the header to read from
 * \return false for a badly formed theme, true otherwise
 */
bool ro_gui_theme_read_file_header(struct theme_descriptor *descriptor,
		struct theme_file_header *file_header)
{
	if ((file_header->magic_value != 0x4d54534e) ||
			(file_header->parser_version > 2))
		return false;

	strcpy(descriptor->name, file_header->name);
	strcpy(descriptor->author, file_header->author);
	descriptor->browser_background = file_header->browser_bg;
	descriptor->hotlist_background = file_header->hotlist_bg;
	descriptor->status_background = file_header->status_bg;
	descriptor->status_foreground = file_header->status_fg;
	descriptor->decompressed_size = file_header->decompressed_sprite_size;
	descriptor->compressed_size = file_header->compressed_sprite_size;
	if (file_header->parser_version >= 2) {
		descriptor->throbber_right =
				!(file_header->theme_flags & (1 << 0));
		descriptor->throbber_redraw =
				file_header->theme_flags & (1 << 1);
	} else {
		descriptor->throbber_right =
				(file_header->theme_flags == 0x00);
		descriptor->throbber_redraw = true;
	}
	return true;
}


/**
 * Opens a theme ready for use.
 *
 * \param descriptor  the theme_descriptor to open
 * \param list	      whether to open all themes in the list
 * \return whether the operation was successful
 */
bool ro_gui_theme_open(struct theme_descriptor *descriptor, bool list)
{
	fileswitch_object_type obj_type;
	squash_output_status status;
	os_coord dimensions;
	os_mode mode;
	os_error *error;
	struct theme_descriptor *next_descriptor;
	char sprite_name[16];
	const char *name = sprite_name;
	bool result = true;
	int i, n;
	int workspace_size, file_size;
	char *raw_data, *workspace;
	osspriteop_area *decompressed;

	/*	If we are freeing the whole of the list then we need to
		start at the first descriptor.
	*/
	if (list && descriptor)
		while (descriptor->previous) descriptor = descriptor->previous;

	/*	Open the themes
	*/
	for (; descriptor; descriptor = next_descriptor) {
		/* see if we should iterate through the entire list */
		if (list)
			next_descriptor = descriptor->next;
		else
			next_descriptor = NULL;

		/* if we are already loaded, increase the usage count */
		if (descriptor->theme) {
			descriptor->theme->users = descriptor->theme->users + 1;
			continue;
		}

		/* create a new theme */
		descriptor->theme = (struct theme *)calloc(1,
				sizeof(struct theme));
		if (!descriptor->theme) {
			NSLOG(netsurf, INFO, "calloc() failed");
			ro_warn_user("NoMemory", 0);
			continue;
		}
		descriptor->theme->users = 1;

		/* try to load the associated file */
		error = xosfile_read_stamped_no_path(descriptor->filename,
				&obj_type, 0, 0, &file_size, 0, 0);
		if (error) {
			NSLOG(netsurf, INFO,
			      "xosfile_read_stamped_no_path: 0x%x: %s",
			      error->errnum,
			      error->errmess);
			ro_warn_user("FileError", error->errmess);
			continue;
		}
		if (obj_type != fileswitch_IS_FILE)
			continue;
		raw_data = malloc(file_size);
		if (!raw_data) {
			NSLOG(netsurf, INFO, "malloc() failed");
			ro_warn_user("NoMemory", 0);
			continue;
		}
		error = xosfile_load_stamped_no_path(descriptor->filename,
				(byte *)raw_data, 0, 0, 0, 0, 0);
		if (error) {
			free(raw_data);
			NSLOG(netsurf, INFO,
			      "xosfile_load_stamped_no_path: 0x%x: %s",
			      error->errnum,
			      error->errmess);
			ro_warn_user("FileError", error->errmess);
			continue;
		}

		/* decompress the new data */
		error = xsquash_decompress_return_sizes(-1, &workspace_size, 0);
		if (error) {
			free(raw_data);
			NSLOG(netsurf, INFO,
			      "xsquash_decompress_return_sizes: 0x%x: %s",
			      error->errnum,
			      error->errmess);
			ro_warn_user("MiscError", error->errmess);
			continue;
		}
		decompressed = (osspriteop_area *)malloc(
				descriptor->decompressed_size);
		workspace = malloc(workspace_size);
		if ((!decompressed) || (!workspace)) {
			free(decompressed);
			free(raw_data);
			NSLOG(netsurf, INFO, "malloc() failed");
			ro_warn_user("NoMemory", 0);
			continue;
		}
		error = xsquash_decompress(squash_INPUT_ALL_PRESENT, workspace,
				(byte *)(raw_data + sizeof(
						struct theme_file_header)),
				descriptor->compressed_size,
				(byte *)decompressed,
				descriptor->decompressed_size,
				&status, 0, 0, 0, 0);
		free(workspace);
		free(raw_data);
		if (error) {
			free(decompressed);
			NSLOG(netsurf, INFO, "xsquash_decompress: 0x%x: %s",
			      error->errnum, error->errmess);
			ro_warn_user("MiscError", error->errmess);
			continue;
		}
		if (status != 0) {
			free(decompressed);
			continue;
		}
		descriptor->theme->sprite_area = decompressed;

		/* find the highest sprite called 'throbber%i', and get the
		 * maximum dimensions for all 'thobber%i' icons. */
		for (i = 1; i <= descriptor->theme->sprite_area->sprite_count;
				i++) {
			error = xosspriteop_return_name(osspriteop_USER_AREA,
					descriptor->theme->sprite_area,
					sprite_name, 16, i, 0);
			if (error) {
				NSLOG(netsurf, INFO,
				      "xosspriteop_return_name: 0x%x: %s",
				      error->errnum,
				      error->errmess);
				ro_warn_user("MiscError", error->errmess);
				continue;
			}
			if (strncmp(sprite_name, "throbber", 8))
				continue;

			/* get the max sprite width/height */
			error = xosspriteop_read_sprite_info(
					osspriteop_USER_AREA,
					descriptor->theme->sprite_area,
					(osspriteop_id) name,
					&dimensions.x, &dimensions.y,
					(osbool *) 0, &mode);
			if (error) {
				NSLOG(netsurf, INFO,
				      "xosspriteop_read_sprite_info: 0x%x: %s",
				      error->errnum,
				      error->errmess);
				ro_warn_user("MiscError", error->errmess);
				continue;
			}
			ro_convert_pixels_to_os_units(&dimensions, mode);
			if (descriptor->theme->throbber_width <	dimensions.x)
				descriptor->theme->throbber_width =
						dimensions.x;
			if (descriptor->theme->throbber_height < dimensions.y)
				descriptor->theme->throbber_height =
						dimensions.y;

			/* get the throbber number */
			n = atoi(sprite_name + 8);
			if (descriptor->theme->throbber_frames < n)
				descriptor->theme->throbber_frames = n;
		}
	}
	return result;
}


/**
 * Applies the theme to all current windows and subsequent ones.
 *
 * \param descriptor  the theme_descriptor to open
 * \return whether the operation was successful
 */
bool ro_gui_theme_apply(struct theme_descriptor *descriptor)
{
	struct theme_descriptor *theme_previous;

	/* check if the theme is already applied */
	if (descriptor == theme_current)
		return true;

	/* re-open the new-theme and release the current theme */
	if (!ro_gui_theme_open(descriptor, false))
		return false;
	theme_previous = theme_current;
	theme_current = descriptor;

	/* apply the theme to all the current toolbar-ed windows */
	ro_toolbar_theme_update();

	ro_gui_theme_close(theme_previous, false);
	return true;
}


/**
 * Closes a theme after use.
 *
 * \param descriptor  the theme_descriptor to close
 * \param list	      whether to open all themes in the list
 * \return whether the operation was successful
 */
void ro_gui_theme_close(struct theme_descriptor *descriptor, bool list)
{

	if (!descriptor)
		return;

	/* move to the start of the list */
	while (list && descriptor->previous)
		descriptor = descriptor->previous;

	/* close the themes */
	while (descriptor) {
		if (descriptor->theme) {
			descriptor->theme->users = descriptor->theme->users - 1;
			if (descriptor->theme->users <= 0) {
				free(descriptor->theme->sprite_area);
				free(descriptor->theme);
				descriptor->theme = NULL;
			}
		}
		if (!list)
			return;
		descriptor = descriptor->next;
	}
}


/**
 * Frees any unused theme descriptors.
 *
 * \param descriptor  the theme_descriptor to free
 */
void ro_gui_theme_free(struct theme_descriptor *descriptor)
{
	struct theme_descriptor *next_descriptor;

	if (!descriptor)
		return;

	/* move to the start of the list */
	while (descriptor->previous)
		descriptor = descriptor->previous;

	/* free closed themes */
	for (; descriptor; descriptor = next_descriptor) {
		next_descriptor = descriptor->next;

		/* no theme? no descriptor */
		if (!descriptor->theme) {
			if (descriptor->previous)
				descriptor->previous->next = descriptor->next;
			if (descriptor->next)
				descriptor->next->previous =
						descriptor->previous;

			/* keep the cached list in sync */
			if (theme_descriptors == descriptor)
				theme_descriptors = next_descriptor;

			/* release any memory */
			free(descriptor->filename);
			free(descriptor);
		}
	}
}


