/* character level styles for Lynx
 * (c) 1996 Rob Partington -- donated to the Lyncei (if they want it :-)
 * $Id: LYStyle.c 1.56 Thu, 06 May 2004 17:31:33 -0700 dickey $
 */
#include <HTUtils.h>
#include <HTML.h>
#include <LYGlobalDefs.h>

#include <LYStructs.h>
#include <LYReadCFG.h>
#include <LYCurses.h>
#include <LYCharUtils.h>
#include <LYUtils.h>		/* defines TABLESIZE */
#include <AttrList.h>
#include <SGML.h>
#include <HTMLDTD.h>

/* Hash table definitions */
#include <LYHash.h>
#include <LYStyle.h>

#include <LYexit.h>
#include <LYLeaks.h>
#include <LYStrings.h>

#ifdef USE_COLOR_STYLE

static void style_initialiseHashTable(void);

/* stack of attributes during page rendering */
int last_styles[128] =
{0};
int last_colorattr_ptr = 0;

bucket hashStyles[CSHASHSIZE];
bucket special_bucket =
{
    "<special>",		/* in order something to be in trace. */
    0, 0, 0, 0, NULL
};
bucket nostyle_bucket =
{
    "<NOSTYLE>",		/* in order something to be in trace. */
    0, 0, 0, 0, NULL
};

int cached_tag_styles[HTML_ELEMENTS];
int current_tag_style;
BOOL force_current_tag_style = FALSE;
char *forced_classname;
BOOL force_classname;

/* Remember the hash codes for common elements */
int s_a = NOSTYLE;
int s_aedit = NOSTYLE;
int s_aedit_arr = NOSTYLE;
int s_aedit_pad = NOSTYLE;
int s_aedit_sel = NOSTYLE;
int s_alert = NOSTYLE;
int s_alink = NOSTYLE;
int s_curedit = NOSTYLE;
int s_forw_backw = NOSTYLE;
int s_hot_paste = NOSTYLE;
int s_menu_active = NOSTYLE;
int s_menu_bg = NOSTYLE;
int s_menu_entry = NOSTYLE;
int s_menu_frame = NOSTYLE;
int s_menu_number = NOSTYLE;
int s_menu_sb = NOSTYLE;
int s_normal = NOSTYLE;
int s_prompt_edit = NOSTYLE;
int s_prompt_edit_arr = NOSTYLE;
int s_prompt_edit_pad = NOSTYLE;
int s_prompt_sel = NOSTYLE;
int s_status = NOSTYLE;
int s_title = NOSTYLE;
int s_whereis = NOSTYLE;

#ifdef USE_SCROLLBAR
int s_sb_aa = NOSTYLE;
int s_sb_bar = NOSTYLE;
int s_sb_bg = NOSTYLE;
int s_sb_naa = NOSTYLE;
#endif

/* start somewhere safe */
#define MAX_COLOR 16
static int colorPairs = 0;

#ifdef USE_BLINK
#  define MAX_BLINK	2
#  define M_BLINK	A_BLINK
#else
#  define MAX_BLINK	1
#  define M_BLINK	0
#endif

static unsigned char our_pairs[2]
[MAX_BLINK]
[MAX_COLOR + 1]
[MAX_COLOR + 1];

/*
 * Parse a string containing a combination of video attributes and color.
 */
static void parse_either(char *attrs,
			 int dft_color,
			 int *monop,
			 int *colorp)
{
    int value;

    while (*attrs != '\0') {
	char *next = strchr(attrs, '+');
	char save = (next != NULL) ? *next : '\0';

	if (next == NULL)
	    next = attrs + strlen(attrs);

	if (save != 0)		/* attrs might be a constant string */
	    *next = '\0';
	if ((value = string_to_attr(attrs)) != 0)
	    *monop |= value;
	else if (colorp != 0
		 && (value = check_color(attrs, dft_color)) != ERR_COLOR)
	    *colorp = value;

	attrs = next;
	if (save != '\0')
	    *attrs++ = save;
    }
}

/* icky parsing of the style options */
static void parse_attributes(char *mono,
			     char *fg,
			     char *bg,
			     int style,
			     char *element)
{
    int mA = A_NORMAL;
    int fA = default_fg;
    int bA = default_bg;
    int cA = A_NORMAL;
    int newstyle = hash_code(element);

    CTRACE2(TRACE_STYLE, (tfp, "CSS(PA):style d=%d / h=%d, e=%s\n",
			  style, newstyle, element));

    parse_either(mono, ERR_COLOR, &mA, (int *) 0);
    parse_either(bg, default_bg, &cA, &bA);
    parse_either(fg, default_fg, &cA, &fA);

    if (style == -1) {		/* default */
	CTRACE2(TRACE_STYLE, (tfp, "CSS(DEF):default_fg=%d, default_bg=%d\n",
			      fA, bA));
	default_fg = fA;
	default_bg = bA;
	default_color_reset = TRUE;
	return;
    }
    if (fA == NO_COLOR) {
	bA = NO_COLOR;
    } else if (COLORS) {
#ifdef USE_BLINK
	if (term_blink_is_boldbg) {
	    if (fA >= COLORS)
		cA = A_BOLD;
	    if (bA >= COLORS)
		cA |= M_BLINK;
	} else
#endif
	if (fA >= COLORS || bA >= COLORS)
	    cA = A_BOLD;
	if (fA >= COLORS)
	    fA %= COLORS;
	if (bA >= COLORS)
	    bA %= COLORS;
    } else {
	cA = A_BOLD;
	fA = NO_COLOR;
	bA = NO_COLOR;
    }

    /*
     * If we have colour, and space to create a new colour attribute,
     * and we have a valid colour description, then add this style
     */
    if (lynx_has_color && colorPairs < COLOR_PAIRS - 1 && fA != NO_COLOR) {
	int curPair = 0;
	int iFg = (1 + (fA >= 0 ? fA : 0));
	int iBg = (1 + (bA >= 0 ? bA : 0));
	int iBold = !!(cA & A_BOLD);
	int iBlink = !!(cA & M_BLINK);

	CTRACE2(TRACE_STYLE, (tfp, "parse_attributes %d/%d %d/%d %#x\n",
			      fA, default_fg, bA, default_bg, cA));
	if (fA < MAX_COLOR
	    && bA < MAX_COLOR
#ifdef USE_CURSES_PAIR_0
	    && (cA != A_NORMAL || fA != default_fg || bA != default_bg)
#endif
	    && curPair < 255) {
	    if (our_pairs[iBold][iBlink][iFg][iBg] != 0) {
		curPair = our_pairs[iBold][iBlink][iFg][iBg];
	    } else {
		curPair = ++colorPairs;
		init_pair((short) curPair, (short) fA, (short) bA);
		our_pairs[iBold][iBlink][iFg][iBg] = curPair;
	    }
	}
	CTRACE2(TRACE_STYLE, (tfp, "CSS(CURPAIR):%d\n", curPair));
	if (style < DSTYLE_ELEMENTS)
	    setStyle(style, COLOR_PAIR(curPair) | cA, cA, mA);
	setHashStyle(newstyle, COLOR_PAIR(curPair) | cA, cA, mA, element);
    } else {
	if (lynx_has_color && fA != NO_COLOR) {
	    CTRACE2(TRACE_STYLE,
		    (tfp, "CSS(NC): maximum of %d colorpairs exhausted\n",
		     COLOR_PAIRS - 1));
	}
	/* only mono is set */
	if (style < DSTYLE_ELEMENTS)
	    setStyle(style, -1, -1, mA);
	setHashStyle(newstyle, -1, -1, mA, element);
    }
}

/* parse a style option of the format
 * STYLE:<OBJECT>:FG:BG
 */
static void parse_style(char *param)
{
    /* *INDENT-OFF* */
    static struct {
	char *name;
	int style;
	int *set_hash;
    } table[] = {
	{ "default",		-1,			0 }, /* default fg/bg */
	{ "alink",		DSTYLE_ALINK,		0 }, /* active link */
	{ "a",			DSTYLE_LINK,		0 }, /* normal link */
	{ "a",			HTML_A,			0 }, /* normal link */
	{ "status",		DSTYLE_STATUS,		0 }, /* status bar */
	{ "label",		DSTYLE_OPTION,		0 }, /* [INLINE]'s */
	{ "value",		DSTYLE_VALUE,		0 }, /* [INLINE]'s */
	{ "high",		DSTYLE_HIGH,		0 }, /* [INLINE]'s */
	{ "normal",		DSTYLE_NORMAL,		0 },
	{ "candy",		DSTYLE_CANDY,		0 }, /* [INLINE]'s */
	{ "whereis",		DSTYLE_WHEREIS,		&s_whereis },
	{ "edit.active.pad",	DSTYLE_ELEMENTS,	&s_aedit_pad },
	{ "edit.active.arrow",	DSTYLE_ELEMENTS,	&s_aedit_arr },
	{ "edit.active.marked",	DSTYLE_ELEMENTS,	&s_aedit_sel },
	{ "edit.active",	DSTYLE_ELEMENTS,	&s_aedit },
	{ "edit.current",	DSTYLE_ELEMENTS,	&s_curedit },
	{ "edit.prompt.pad",	DSTYLE_ELEMENTS,	&s_prompt_edit_pad },
	{ "edit.prompt.arrow",	DSTYLE_ELEMENTS,	&s_prompt_edit_arr },
	{ "edit.prompt.marked",	DSTYLE_ELEMENTS,	&s_prompt_sel },
	{ "edit.prompt",	DSTYLE_ELEMENTS,	&s_prompt_edit },
	{ "forwbackw.arrow",	DSTYLE_ELEMENTS,	&s_forw_backw },
	{ "hot.paste",		DSTYLE_ELEMENTS,	&s_hot_paste },
	{ "menu.frame",		DSTYLE_ELEMENTS,	&s_menu_frame },
	{ "menu.bg",		DSTYLE_ELEMENTS,	&s_menu_bg },
	{ "menu.n",		DSTYLE_ELEMENTS,	&s_menu_number },
	{ "menu.entry",		DSTYLE_ELEMENTS,	&s_menu_entry },
	{ "menu.active",	DSTYLE_ELEMENTS,	&s_menu_active },
	{ "menu.sb",		DSTYLE_ELEMENTS,	&s_menu_sb },
    };
    /* *INDENT-ON* */

    unsigned n;
    BOOL found = FALSE;

    char *buffer = 0;
    char *tmp = 0;
    char *element, *mono, *fg, *bg;

    if (param == 0)
	return;
    CTRACE2(TRACE_STYLE, (tfp, "parse_style(%s)\n", param));
    StrAllocCopy(buffer, param);
    if (buffer == 0)
	return;

    if ((tmp = strchr(buffer, ':')) == 0) {
	fprintf(stderr, gettext("\
Syntax Error parsing style in lss file:\n\
[%s]\n\
The line must be of the form:\n\
OBJECT:MONO:COLOR (ie em:bold:brightblue:white)\n\
where OBJECT is one of EM,STRONG,B,I,U,BLINK etc.\n\n"), buffer);
	if (!dump_output_immediately) {
	    exit_immediately(EXIT_FAILURE);
	}
	exit(1);
    }
    strtolower(buffer);
    *tmp = '\0';
    element = buffer;

    mono = tmp + 1;
    tmp = strchr(mono, ':');

    if (!tmp) {
	fg = "nocolor";
	bg = "nocolor";
    } else {
	*tmp = '\0';
	fg = tmp + 1;
	tmp = strchr(fg, ':');
	if (!tmp)
	    bg = "default";
	else {
	    *tmp = '\0';
	    bg = tmp + 1;
	}
    }

    CTRACE2(TRACE_STYLE, (tfp, "CSSPARSE:%s => %d %s\n",
			  element, hash_code(element),
			  (hashStyles[hash_code(element)].name ? "used" : "")));

    strtolower(element);

    /*
     * We use some pseudo-elements, so catch these first
     */
    for (n = 0; n < TABLESIZE(table); n++) {
	if (!strcasecomp(element, table[n].name)) {
	    parse_attributes(mono, fg, bg, table[n].style, table[n].name);
	    if (table[n].set_hash != 0)
		*(table[n].set_hash) = hash_code(table[n].name);
	    found = TRUE;
	    break;
	}
    }

    if (found) {
	;
    } else if (!strcasecomp(element, "normal"))		/* added - kw */
    {
	parse_attributes(mono, fg, bg, DSTYLE_NORMAL, "html");
	s_normal = hash_code("html");	/* rather bizarre... - kw */
    }
    /* It must be a HTML element, so look through the list until we find it. */
    else {
	int element_number = -1;
	HTTag *t = SGMLFindTag(&HTML_dtd, element);

	if (t && t->name) {
	    element_number = t - HTML_dtd.tags;
	}
	if (element_number >= HTML_A &&
	    element_number < HTML_ELEMENTS)
	    parse_attributes(mono, fg, bg, element_number + STARTAT, element);
	else
	    parse_attributes(mono, fg, bg, DSTYLE_ELEMENTS, element);
    }
    FREE(buffer);
}

#ifdef LY_FIND_LEAKS
static void free_colorstylestuff(void)
{
    style_initialiseHashTable();
    style_deleteStyleList();
}
#endif

/*
 * initialise the default style sheet
 * This should be able to be read from a file in CSS format :-)
 */
static void initialise_default_stylesheet(void)
{
    static const char *table[] =
    {
	"a:bold:green",
	"alert:bold:yellow:red",
	"alink:reverse:yellow:black",
	"label:normal:magenta",
	"status:reverse:yellow:blue",
	"title:normal:magenta",
	"whereis:reverse+underline:magenta:cyan"
    };
    unsigned n;
    char temp[80];

    CTRACE((tfp, "initialize_default_stylesheet\n"));
    for (n = 0; n < TABLESIZE(table); n++) {
	parse_style(strcpy(temp, table[n]));
    }
}

/* Set all the buckets in the hash table to be empty */
static void style_initialiseHashTable(void)
{
    int i;
    static int firsttime = 1;

    for (i = 0; i < CSHASHSIZE; i++) {
	if (firsttime)
	    hashStyles[i].name = NULL;
	else
	    FREE(hashStyles[i].name);
	hashStyles[i].color = 0;
	hashStyles[i].cattr = 0;
	hashStyles[i].mono = 0;
    }
    if (firsttime) {
	firsttime = 0;
#ifdef LY_FIND_LEAKS
	atexit(free_colorstylestuff);
#endif
    }
    s_alink = hash_code("alink");
    s_a = hash_code("a");
    s_status = hash_code("status");
    s_alert = hash_code("alert");
    s_title = hash_code("title");
#ifdef USE_SCROLLBAR
    s_sb_bar = hash_code("scroll.bar");
    s_sb_bg = hash_code("scroll.back");
    s_sb_aa = hash_code("scroll.arrow");
    s_sb_naa = hash_code("scroll.noarrow");
#endif
}

/* because curses isn't started when we parse the config file, we
 * need to remember the STYLE: lines we encounter and parse them
 * after curses has started
 */
static HTList *lss_styles = NULL;

void parse_userstyles(void)
{
    char *name;
    HTList *cur = lss_styles;

    colorPairs = 0;
    style_initialiseHashTable();

    /* set our styles to be the same as vanilla-curses-lynx */
    if (HTList_isEmpty(cur)) {
	initialise_default_stylesheet();
    } else {
	while ((name = HTList_nextObject(cur)) != NULL) {
	    CTRACE2(TRACE_STYLE, (tfp, "LSS:%s\n",
				  (name
				   ? name
				   : "!?! empty !?!")));
	    if (name != NULL)
		parse_style(name);
	}
    }

#define dft_style(a,b) if (a == NOSTYLE) a = b
    /* *INDENT-OFF* */
    dft_style(s_prompt_edit,		s_normal);
    dft_style(s_prompt_edit_arr,	s_prompt_edit);
    dft_style(s_prompt_edit_pad,	s_prompt_edit);
    dft_style(s_prompt_sel,		s_prompt_edit);
    dft_style(s_aedit,			s_alink);
    dft_style(s_aedit_arr,		s_aedit);
    dft_style(s_aedit_pad,		s_aedit);
    dft_style(s_curedit,		s_aedit);
    dft_style(s_aedit_sel,		s_aedit);
    dft_style(s_menu_bg,		s_normal);
    dft_style(s_menu_entry,		s_menu_bg);
    dft_style(s_menu_frame,		s_menu_bg);
    dft_style(s_menu_number,		s_menu_bg);
    dft_style(s_menu_active,		s_alink);
    /* *INDENT-ON* */

}

/* Add a STYLE: option line to our list.  Process "default:" early
 * for it to have the same semantic as other lines: works at any place
 * of the style file, the first line overrides the later ones.
 */
static void HStyle_addStyle(char *buffer)
{
    char *name = NULL;

    CTRACE((tfp, "HStyle_addStyle(%s)\n", buffer));
    StrAllocCopy(name, buffer);
    if (lss_styles == NULL)
	lss_styles = HTList_new();
    strtolower(name);
    if (!strncasecomp(name, "default:", 8))	/* default fg/bg */
    {
	CTRACE2(TRACE_STYLE, (tfp, "READCSS.default%s:%s\n",
			      (default_color_reset ? ".ignore" : ""),
			      name ? name : "!?! empty !?!"));
	if (!default_color_reset)
	    parse_style(name);
	return;			/* do not need to process it again */
    }
    CTRACE2(TRACE_STYLE, (tfp, "READCSS:%s\n", name ? name : "!?! empty !?!"));
    HTList_addObject(lss_styles, name);
}

void style_deleteStyleList(void)
{
    char *name;

    while ((name = HTList_removeLastObject(lss_styles)) != NULL)
	FREE(name);
    HTList_delete(lss_styles);
    lss_styles = NULL;
}

static int style_readFromFileREC(char *lss_filename,
				 char *parent_filename)
{
    FILE *fh;
    char *buffer = NULL;
    int len;

    CTRACE2(TRACE_STYLE, (tfp, "CSS:Reading styles from file: %s\n",
			  lss_filename ? lss_filename : "?!? empty ?!?"));
    if (isEmpty(lss_filename))
	return -1;
    if ((fh = LYOpenCFG(lss_filename, parent_filename, LYNX_LSS_FILE)) == 0) {
	/* this should probably be an alert or something */
	CTRACE2(TRACE_STYLE, (tfp,
			      "CSS:Can't open style file '%s', using defaults\n", lss_filename));
	return -1;
    }

    if (parent_filename == 0) {
	style_initialiseHashTable();
	style_deleteStyleList();
    }

    while (LYSafeGets(&buffer, fh) != NULL) {
	LYTrimTrailing(buffer);
	LYTrimTail(buffer);
	LYTrimHead(buffer);
	if (!strncasecomp(buffer, "include:", 8))
	    style_readFromFileREC(buffer + 8, lss_filename);
	else if (buffer[0] != '#' && (len = strlen(buffer)) > 0)
	    HStyle_addStyle(buffer);
    }

    LYCloseInput(fh);
    if ((parent_filename == 0) && LYCursesON)
	parse_userstyles();
    return 0;
}

int style_readFromFile(char *filename)
{
    return style_readFromFileREC(filename, (char *) 0);
}

/* Used in HTStructured methods: - kw */

void TrimColorClass(const char *tagname,
		    char *styleclassname,
		    int *phcode)
{
    char *end, *start = NULL, *lookfrom;
    char tmp[64];

    sprintf(tmp, ";%.*s", (int) sizeof(tmp) - 3, tagname);
    strtolower(tmp);

    if ((lookfrom = styleclassname) != 0) {
	do {
	    end = start;
	    start = strstr(lookfrom, tmp);
	    if (start)
		lookfrom = start + 1;
	}
	while (start);
	/* trim the last matching element off the end
	 * - should match classes here as well (rp)
	 */
	if (end)
	    *end = '\0';
    }
    *phcode = hash_code(lookfrom && *lookfrom ? lookfrom : &tmp[1]);
}

/* This function is designed as faster analog to TrimColorClass.
 * It assumes that tag_name is present in stylename! -HV
 */
void FastTrimColorClass(const char *tag_name,
			int name_len,
			char *stylename,
			char **pstylename_end,	/*will be modified */
			int *phcode)	/*will be modified */
{
    char *tag_start = *pstylename_end;
    BOOLEAN found = FALSE;

    CTRACE2(TRACE_STYLE,
	    (tfp, "STYLE.fast-trim: [%s] from [%s]: ",
	     tag_name, stylename));
    while (tag_start >= stylename) {
	for (; (tag_start >= stylename) && (*tag_start != ';'); --tag_start) ;
	if (!strncasecomp(tag_start + 1, tag_name, name_len)) {
	    found = TRUE;
	    break;
	}
	--tag_start;
    }
    if (found) {
	*tag_start = '\0';
	*pstylename_end = tag_start;
    }
    CTRACE2(TRACE_STYLE, (tfp, found ? "success.\n" : "failed.\n"));
    *phcode = hash_code(tag_start + 1);
}

 /* This is called each time lss styles are read. It will fill
  * each elt of 'cached_tag_styles' -HV
  */
void cache_tag_styles(void)
{
    char buf[200];
    int i;

    for (i = 0; i < HTML_ELEMENTS; ++i) {
	LYstrncpy(buf, HTML_dtd.tags[i].name, sizeof(buf) - 1);
	LYLowerCase(buf);
	cached_tag_styles[i] = hash_code(buf);
    }
}

#endif /* USE_COLOR_STYLE */
