/*
 textbuffer-view.c : Text buffer handling

    Copyright (C) 1999-2001 Timo Sirainen

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

    This program 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, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

#define	G_LOG_DOMAIN "TextBufferView"

#include "module.h"
#include <irssi/src/core/levels.h>
#include <irssi/src/core/signals.h>
#include <irssi/src/core/utf8.h>
#include <irssi/src/fe-common/core/formats.h>
#include <irssi/src/fe-text/textbuffer-formats.h>
#include <irssi/src/fe-text/textbuffer-view.h>

typedef struct {
	char *name;
        LINE_REC *line;
} BOOKMARK_REC;

/* how often to scan line cache for lines not accessed for a while (ms) */
#define LINE_CACHE_CHECK_TIME (5*60*1000)
/* how long to keep line cache in memory (seconds) */
#define LINE_CACHE_KEEP_TIME (10*60)

static int linecache_tag;
static GSList *views;

#define view_is_bottom(view) \
        ((view)->ypos >= -1 && (view)->ypos < (view)->height)

#define view_get_linecount_hidden(view, line) \
        textbuffer_view_get_line_cache(view, line)->count

#define view_line_is_hidden(view, line) \
	(((line)->info.level & (view)->hidden_level) != 0)

#define view_get_linecount(view, line) \
	(view_line_is_hidden(view, line) ? 0 : view_get_linecount_hidden(view, line))

static GSList *textbuffer_get_views(TEXT_BUFFER_REC *buffer)
{
	GSList *tmp, *list;

	for (tmp = views; tmp != NULL; tmp = tmp->next) {
		TEXT_BUFFER_VIEW_REC *view = tmp->data;

		if (view->buffer == buffer) {
			list = g_slist_copy(view->siblings);
                        return g_slist_prepend(list, view);
		}
	}

        return NULL;
}

static TEXT_BUFFER_CACHE_REC *
textbuffer_cache_get(GSList *views, int width)
{
	TEXT_BUFFER_CACHE_REC *cache;

        /* check if there's existing cache with correct width */
	while (views != NULL) {
		TEXT_BUFFER_VIEW_REC *view = views->data;

		if (view->width == width) {
			view->cache->refcount++;
			return view->cache;
		}
                views = views->next;
	}

        /* create new cache */
	cache = g_new0(TEXT_BUFFER_CACHE_REC, 1);
	cache->refcount = 1;
        cache->width = width;
	cache->line_cache = g_hash_table_new((GHashFunc) g_direct_hash,
					     (GCompareFunc) g_direct_equal);
        return cache;
}

static int line_cache_destroy(void *key, LINE_CACHE_REC *cache)
{
	g_free(cache->line_text);
	g_free(cache);
	return TRUE;
}

static void textbuffer_cache_destroy(TEXT_BUFFER_CACHE_REC *cache)
{
	g_hash_table_foreach(cache->line_cache,
			     (GHFunc) line_cache_destroy, NULL);
	g_hash_table_destroy(cache->line_cache);
        g_free(cache);
}

static void textbuffer_cache_unref(TEXT_BUFFER_CACHE_REC *cache)
{
	if (--cache->refcount == 0)
                textbuffer_cache_destroy(cache);
}

#define FGATTR (ATTR_NOCOLORS | ATTR_RESETFG | FG_MASK | ATTR_FGCOLOR24)
#define BGATTR (ATTR_NOCOLORS | ATTR_RESETBG | BG_MASK | ATTR_BGCOLOR24)

#ifdef TERM_TRUECOLOR
static void unformat_24bit_line_color(const unsigned char **ptr, int off, int *flags, unsigned int *fg, unsigned int *bg)
{
	unsigned int color;
	unsigned char rgbx[4];
	unsigned int i;
	for (i = 0; i < 4; ++i) {
		if ((*ptr)[i + off] == '\0')
			return;
		rgbx[i] = (*ptr)[i + off];
	}
	rgbx[3] -= 0x20;
	*ptr += 4;
	for (i = 0; i < 3; ++i) {
		if (rgbx[3] & (0x10 << i))
			rgbx[i] -= 0x20;
	}
	color = rgbx[0] << 16 | rgbx[1] << 8 | rgbx[2];
	if (rgbx[3] & 0x1) {
		*flags = (*flags & FGATTR) | ATTR_BGCOLOR24;
		*bg = color;
	}
	else {
		*flags = (*flags & BGATTR) | ATTR_FGCOLOR24;
		*fg = color;
	}
}
#endif

static inline unichar read_unichar(const unsigned char *data, const unsigned char **next, int *width)
{
	unichar chr = g_utf8_get_char_validated((const char *) data, -1);

	if (chr & 0x80000000) {
		chr = 0xfffd;
		*next = data + 1;
		*width = 1;
	} else {
		*next = (unsigned char *)g_utf8_next_char(data);
		*width = unichar_isprint(chr) ? i_wcwidth(chr) : 1;
	}
	return chr;
}

static inline void unformat(const unsigned char **ptr, int *color, unsigned int *fg24,
                            unsigned int *bg24)
{
	switch (**ptr) {
	case FORMAT_STYLE_BLINK:
		*color ^= ATTR_BLINK;
		break;
	case FORMAT_STYLE_UNDERLINE:
		*color ^= ATTR_UNDERLINE;
		break;
	case FORMAT_STYLE_BOLD:
		*color ^= ATTR_BOLD;
		break;
	case FORMAT_STYLE_REVERSE:
		*color ^= ATTR_REVERSE;
		break;
	case FORMAT_STYLE_ITALIC:
		*color ^= ATTR_ITALIC;
		break;
	case FORMAT_STYLE_MONOSPACE:
		/* *color ^= ATTR_MONOSPACE; */
		break;
	case FORMAT_STYLE_DEFAULTS:
		*color = ATTR_RESET;
		break;
	case FORMAT_STYLE_CLRTOEOL:
		break;
#define SET_COLOR_EXT_FG_BITS(base, pc)                                                            \
	*color &= ~ATTR_FGCOLOR24;                                                                 \
	*color = (*color & BGATTR) | (base + *pc - FORMAT_COLOR_NOCHANGE)
#define SET_COLOR_EXT_BG_BITS(base, pc)                                                            \
	*color &= ~ATTR_BGCOLOR24;                                                                 \
	*color = (*color & FGATTR) | ((base + *pc - FORMAT_COLOR_NOCHANGE) << BG_SHIFT)
	case FORMAT_COLOR_EXT1:
		SET_COLOR_EXT_FG_BITS(0x10, ++*ptr);
		break;
	case FORMAT_COLOR_EXT1_BG:
		SET_COLOR_EXT_BG_BITS(0x10, ++*ptr);
		break;
	case FORMAT_COLOR_EXT2:
		SET_COLOR_EXT_FG_BITS(0x60, ++*ptr);
		break;
	case FORMAT_COLOR_EXT2_BG:
		SET_COLOR_EXT_BG_BITS(0x60, ++*ptr);
		break;
	case FORMAT_COLOR_EXT3:
		SET_COLOR_EXT_FG_BITS(0xb0, ++*ptr);
		break;
	case FORMAT_COLOR_EXT3_BG:
		SET_COLOR_EXT_BG_BITS(0xb0, ++*ptr);
		break;
#undef SET_COLOR_EXT_BG_BITS
#undef SET_COLOR_EXT_FG_BITS
#ifdef TERM_TRUECOLOR
	case FORMAT_COLOR_24:
		unformat_24bit_line_color(ptr, 1, color, fg24, bg24);
		break;
#endif
	default:
		if (**ptr != FORMAT_COLOR_NOCHANGE) {
			if (**ptr == (unsigned char) 0xff) {
				*color = (*color & BGATTR) | ATTR_RESETFG;
			} else {
				*color = (*color & BGATTR) | (((unsigned char) **ptr - '0') & 0xf);
			}
		}
		if ((*ptr)[1] == '\0')
			break;

		(*ptr)++;
		if (**ptr != FORMAT_COLOR_NOCHANGE) {
			if (**ptr == (unsigned char) 0xff) {
				*color = (*color & FGATTR) | ATTR_RESETBG;
			} else {
				*color = (*color & FGATTR) |
				         ((((unsigned char) **ptr - '0') & 0xf) << BG_SHIFT);
			}
		}
	}
	if (**ptr == '\0')
		return;

	(*ptr)++;
}

#define NEXT_CHAR_OR_BREAK(p)                                                                      \
	(p)++;                                                                                     \
	if (*(p) == '\0')                                                                          \
	break

static LINE_CACHE_REC *
view_update_line_cache(TEXT_BUFFER_VIEW_REC *view, LINE_REC *line)
{
        INDENT_FUNC indent_func;
	LINE_CACHE_REC *rec;
	LINE_CACHE_SUB_REC *sub;
	GSList *lines;
	char *line_text;
	const unsigned char *ptr, *next_ptr, *last_space_ptr;
	int xpos, pos, indent_pos, last_space, last_color, color, linecount;
	unsigned int last_bg24, last_fg24, bg24, fg24;
	int char_width;

	color = ATTR_RESETFG | ATTR_RESETBG;
	xpos = 0; indent_pos = view->default_indent;
	last_space = last_color = 0; last_space_ptr = NULL; sub = NULL;
	bg24 = fg24 = last_bg24 = last_fg24 = UINT_MAX;

        indent_func = view->default_indent_func;
        linecount = 1;
	lines = NULL;

	line_text = textbuffer_line_get_text(view->buffer, line, FALSE);
	if (line_text != NULL) {
		for (ptr = (unsigned char *) line_text;;) {
			if (*ptr == '\0')
				break;

			if (*ptr == '\n') {
				/* newline */
				xpos = 0;
				last_space = 0;

				sub = g_new0(LINE_CACHE_SUB_REC, 1);

				sub->start = ++ptr;
				sub->color = color;
#ifdef TERM_TRUECOLOR
				sub->fg24 = fg24;
				sub->bg24 = bg24;
#endif

				lines = g_slist_append(lines, sub);
				linecount++;

				continue;
			}

			if (*ptr == 4) {
				/* format */
				NEXT_CHAR_OR_BREAK(ptr);

				if (*ptr == FORMAT_STYLE_INDENT) {
					/* set indentation position here - don't do
					   it if we're too close to right border */
					if (xpos < view->width - 5)
						indent_pos = xpos;
					ptr++;
				} else {
					unformat(&ptr, &color, &fg24, &bg24);
				}
				continue;
			}

			if (!view->utf8) {
				/* MH */
				if (term_type != TERM_TYPE_BIG5 || ptr[1] == '\0' ||
				    !is_big5(ptr[0], ptr[1]))
					char_width = 1;
				else
					char_width = 2;
				next_ptr = ptr + char_width;
			} else {
				read_unichar(ptr, &next_ptr, &char_width);
			}

			if (xpos + char_width > view->width && sub != NULL &&
			    (last_space <= indent_pos || last_space <= 10) &&
			    view->longword_noindent) {
				/* long word, remove the indentation from this line */
				xpos -= sub->indent;
				sub->indent = 0;
				sub->indent_func = NULL;
			}

			if (xpos + char_width > view->width) {
				xpos =
				    indent_func == NULL ? indent_pos : indent_func(view, line, -1);

				sub = g_new0(LINE_CACHE_SUB_REC, 1);
				if (last_space > indent_pos && last_space > 10) {
					/* go back to last space */
					color = last_color;
					fg24 = last_fg24;
					bg24 = last_bg24;
					ptr = last_space_ptr;
					while (*ptr == ' ')
						ptr++;
				} else if (view->longword_noindent) {
					/* long word, no indentation in next line */
					xpos = 0;
					sub->continues = TRUE;
				}

				sub->start = ptr;
				sub->indent = xpos;
				sub->indent_func = indent_func;
				sub->color = color;
#ifdef TERM_TRUECOLOR
				sub->fg24 = fg24;
				sub->bg24 = bg24;
#endif

				lines = g_slist_append(lines, sub);
				linecount++;

				last_space = 0;
				continue;
			}

			if (view->break_wide && char_width > 1) {
				last_space = xpos;
				last_space_ptr = next_ptr;
				last_color = color;
				last_fg24 = fg24;
				last_bg24 = bg24;
			} else if (*ptr == ' ') {
				last_space = xpos;
				last_space_ptr = ptr;
				last_color = color;
				last_fg24 = fg24;
				last_bg24 = bg24;
			}

			xpos += char_width;
			ptr = next_ptr;
		}
	}

	rec = g_malloc(sizeof(LINE_CACHE_REC)-sizeof(LINE_CACHE_SUB_REC) +
		       sizeof(LINE_CACHE_SUB_REC) * (linecount-1));
	rec->last_access = time(NULL);
	if (line_text == NULL) {
		linecount = 0;
	}
	rec->count = linecount;
	rec->line_text = line_text;

	if (rec->count > 1) {
		for (pos = 0; lines != NULL; pos++) {
			void *data = lines->data;

			memcpy(&rec->lines[pos], data,
			       sizeof(LINE_CACHE_SUB_REC));

			lines = g_slist_remove(lines, data);
			g_free(data);
		}
	}

	g_hash_table_insert(view->cache->line_cache, line, rec);
	return rec;
}

static void view_remove_cache(TEXT_BUFFER_VIEW_REC *view, LINE_REC *line,
			      unsigned char update_counter)
{
	LINE_CACHE_REC *cache;

	if (view->cache->update_counter == update_counter)
		return;
	view->cache->update_counter = update_counter;

	cache = g_hash_table_lookup(view->cache->line_cache, line);
	if (cache != NULL) {
                g_free(cache);
		g_hash_table_remove(view->cache->line_cache, line);
	}
}

static void view_update_cache(TEXT_BUFFER_VIEW_REC *view, LINE_REC *line,
			      unsigned char update_counter)
{
	view_remove_cache(view, line, update_counter);

	if (view->buffer->cur_line == line)
		view->cache->last_linecount = view_get_linecount(view, line);
}

void textbuffer_view_reset_cache(TEXT_BUFFER_VIEW_REC *view)
{
	GSList *tmp;

	/* destroy line caches - note that you can't do simultaneously
	   unrefs + cache_get()s or it will keep using the old caches */
	textbuffer_cache_unref(view->cache);
        g_slist_foreach(view->siblings, (GFunc) textbuffer_cache_unref, NULL);

	view->cache = textbuffer_cache_get(view->siblings, view->width);
	for (tmp = view->siblings; tmp != NULL; tmp = tmp->next) {
		TEXT_BUFFER_VIEW_REC *rec = tmp->data;

		rec->cache = textbuffer_cache_get(rec->siblings, rec->width);
	}
}

static int view_line_draw(TEXT_BUFFER_VIEW_REC *view, LINE_REC *line,
			  int subline, int ypos, int max)
{
	INDENT_FUNC indent_func;
	LINE_CACHE_REC *cache;
	const unsigned char *text, *end, *text_newline;
	unichar chr;
	int xpos, color, drawcount, first, need_move, need_clrtoeol, char_width;
	unsigned int fg24, bg24;

	if (view->dirty) /* don't bother drawing anything - redraw is coming */
		return 0;

	cache = textbuffer_view_get_line_cache(view, line);
	if (subline >= cache->count)
		return 0;

	color = ATTR_RESET;
	need_move = TRUE; need_clrtoeol = FALSE;
	xpos = drawcount = 0; first = TRUE;
	text_newline = text =
	    subline == 0 ? (unsigned char *) cache->line_text : cache->lines[subline - 1].start;

	for (;;) {
		if (text == text_newline) {
			if (need_clrtoeol && xpos < view->width + (view->width == term_width ? 0 : 1)) {
				term_set_color(view->window, ATTR_RESET);
				term_window_clrtoeol(view->window, ypos);
			}

			if (first)
				first = FALSE;
			else {
				ypos++;
				if (--max == 0)
					break;
			}

			if (subline > 0) {
				/* continuing previous line - indent it */
				indent_func = cache->lines[subline-1].indent_func;
				if (indent_func == NULL || cache->lines[subline-1].continues)
					xpos = cache->lines[subline-1].indent;
				color = cache->lines[subline-1].color;
#ifdef TERM_TRUECOLOR
				fg24 = cache->lines[subline-1].fg24;
				bg24 = cache->lines[subline-1].bg24;
#endif
			} else {
				indent_func = NULL;
			}

			if (xpos == 0 && (indent_func == NULL || cache->lines[subline-1].continues))
				need_clrtoeol = TRUE;
			else {
				/* line was indented - need to clear the
				   indented area first */
				term_set_color(view->window, ATTR_RESET);
				term_move(view->window, 0, ypos);
				term_window_clrtoeol(view->window, ypos);

				if (indent_func != NULL)
					xpos = indent_func(view, line, ypos);
			}

			if (need_move || xpos > 0)
				term_move(view->window, xpos, ypos);

			term_set_color2(view->window, color, fg24, bg24);

			if (subline == cache->count-1) {
				text_newline = NULL;
				need_move = FALSE;
			} else {
				/* get the beginning of the next subline */
				text_newline = cache->lines[subline].start;
				if (view->width == term_width) {
					/* ensure that links / long words are not broken */
					need_move = !cache->lines[subline].continues;
				} else {
					/* we cannot use the need_move
					   optimisation unless the split spans
					   the whole width */
					need_move = TRUE;
				}
			}
			drawcount++;
			subline++;
		}

		if (*text == '\n') {
			/* newline */
			NEXT_CHAR_OR_BREAK(text);
			continue;
		}

		if (*text == 0) {
			break;
		}

		if (*text == 4) {
			/* format */
			NEXT_CHAR_OR_BREAK(text);

			if (*text == FORMAT_STYLE_INDENT) {
				/* ??? */
				NEXT_CHAR_OR_BREAK(text);
			} else {
				unformat(&text, &color, &fg24, &bg24);
				term_set_color2(view->window, color, fg24, bg24);
				if (*text == 0)
					break;
			}
			continue;
		}

		if (view->utf8) {
			chr = read_unichar(text, &end, &char_width);
		} else {
			chr = *text;
			end = text;
			if (term_type == TERM_TYPE_BIG5 &&
			    is_big5(end[0], end[1]))
				char_width = 2;
			else
				char_width = 1;
			end += char_width;
		}

		xpos += char_width;
		if (xpos <= view->width) {
			if (unichar_isprint(chr)) {
				if (view->utf8)
					term_add_unichar(view->window, chr);
				else
					for (; text < end; text++)
						term_addch(view->window, *text);
			} else {
				/* low-ascii */
				term_set_color(view->window, ATTR_RESET|ATTR_REVERSE);
				term_addch(view->window, (chr & 127)+'A'-1);
				term_set_color2(view->window, color, fg24, bg24);
			}
		}
		text = end;
	}

	if (need_clrtoeol && xpos < view->width + (view->width == term_width ? 0 : 1)) {
		term_set_color(view->window, ATTR_RESET);
		term_window_clrtoeol(view->window, ypos);
	}

	return drawcount;
}

/* Recalculate view's bottom line information - try to keep the
   original if possible */
static void textbuffer_view_init_bottom(TEXT_BUFFER_VIEW_REC *view)
{
        LINE_REC *line;
        int linecount, total;

	if (view->empty_linecount == 0) {
		/* no empty lines in screen, no need to try to keep
		   the old bottom startline */
                view->bottom_startline = NULL;
	}

	total = 0;
        line = textbuffer_line_last(view->buffer);
	for (; line != NULL; line = line->prev) {
		if (view_line_is_hidden(view, line))
			continue;

		linecount = view_get_linecount(view, line);
		if (line == view->bottom_startline) {
			/* keep the old one, make sure that subline is ok */
			if (view->bottom_subline > linecount)
				view->bottom_subline = linecount;
			view->empty_linecount = view->height - total -
				(linecount-view->bottom_subline);
                        return;
		}

                total += linecount;
		if (total >= view->height) {
			view->bottom_startline = line;
			view->bottom_subline = total - view->height;
                        view->empty_linecount = 0;
                        return;
		}
	}

        /* not enough lines so we must be at the beginning of the buffer */
	view->bottom_startline = view->buffer->first_line;
	view->bottom_subline = 0;
	view->empty_linecount = view->height - total;
}

static void textbuffer_view_init_ypos(TEXT_BUFFER_VIEW_REC *view)
{
        LINE_REC *line;

	g_return_if_fail(view != NULL);

	view->ypos = -view->subline-1;
	for (line = view->startline; line != NULL; line = line->next)
		view->ypos += view_get_linecount(view, line);
}

/* Create new view. */
TEXT_BUFFER_VIEW_REC *textbuffer_view_create(TEXT_BUFFER_REC *buffer,
					     int width, int height,
					     int scroll, int utf8)
{
	TEXT_BUFFER_VIEW_REC *view;

        g_return_val_if_fail(buffer != NULL, NULL);
        g_return_val_if_fail(width > 0, NULL);

	view = g_new0(TEXT_BUFFER_VIEW_REC, 1);
	view->buffer = buffer;
        view->siblings = textbuffer_get_views(buffer);

	view->width = width;
        view->height = height;
	view->scroll = scroll;
        view->utf8 = utf8;

	view->cache = textbuffer_cache_get(view->siblings, width);
	textbuffer_view_init_bottom(view);

	view->startline = view->bottom_startline;
        view->subline = view->bottom_subline;
	view->bottom = TRUE;

	view->hidden_level = 0;

	textbuffer_view_init_ypos(view);

	view->bookmarks = g_hash_table_new((GHashFunc) g_str_hash,
					   (GCompareFunc) g_str_equal);

	views = g_slist_append(views, view);
        return view;
}

/* Destroy the view. */
void textbuffer_view_destroy(TEXT_BUFFER_VIEW_REC *view)
{
	GSList *tmp;

	g_return_if_fail(view != NULL);

	views = g_slist_remove(views, view);

	if (view->siblings == NULL) {
		/* last view for textbuffer, destroy */
                textbuffer_destroy(view->buffer);
	} else {
		/* remove ourself from siblings lists */
		for (tmp = view->siblings; tmp != NULL; tmp = tmp->next) {
			TEXT_BUFFER_VIEW_REC *rec = tmp->data;

			rec->siblings = g_slist_remove(rec->siblings, view);
		}
		g_slist_free(view->siblings);
	}

	g_hash_table_foreach(view->bookmarks, (GHFunc) g_free, NULL);
	g_hash_table_destroy(view->bookmarks);

        textbuffer_cache_unref(view->cache);
	g_free(view);
}

/* Change the default indent position */
void textbuffer_view_set_default_indent(TEXT_BUFFER_VIEW_REC *view,
					int default_indent,
					int longword_noindent,
					INDENT_FUNC indent_func)
{
        if (default_indent != -1)
		view->default_indent = default_indent;
        if (longword_noindent != -1)
		view->longword_noindent = longword_noindent;

	view->default_indent_func = indent_func;
}

/* Enable breaking of wide chars */
void textbuffer_view_set_break_wide(TEXT_BUFFER_VIEW_REC *view,
				    gboolean break_wide)
{
	if (view->break_wide != break_wide) {
		view->break_wide = break_wide;
		textbuffer_view_reset_cache(view);
	}
}

static void view_unregister_indent_func(TEXT_BUFFER_VIEW_REC *view,
					INDENT_FUNC indent_func)
{
	if (view->default_indent_func == indent_func)
		view->default_indent_func = NULL;

	/* recreate cache so it won't contain references
	   to the indent function */
	textbuffer_view_reset_cache(view);
	view->cache = textbuffer_cache_get(view->siblings, view->width);
}

void textbuffer_views_unregister_indent_func(INDENT_FUNC indent_func)
{
	g_slist_foreach(views, (GFunc) view_unregister_indent_func,
			(void *) indent_func);
}

void textbuffer_view_set_scroll(TEXT_BUFFER_VIEW_REC *view, int scroll)
{
        view->scroll = scroll;
}

void textbuffer_view_set_utf8(TEXT_BUFFER_VIEW_REC *view, int utf8)
{
        view->utf8 = utf8;
}

static int view_get_linecount_all(TEXT_BUFFER_VIEW_REC *view, LINE_REC *line)
{
	int linecount;

        linecount = 0;
	while (line != NULL) {
		linecount += view_get_linecount(view, line);
                line = line->next;
	}

        return linecount;
}

static void view_draw(TEXT_BUFFER_VIEW_REC *view, LINE_REC *line,
		      int subline, int ypos, int lines, int fill_bottom)
{
	int linecount;

	if (view->dirty) /* don't bother drawing anything - redraw is coming */
                return;

	while (line != NULL && lines > 0) {
		if (!view_line_is_hidden(view, line)) {
			linecount = view_line_draw(view, line, subline, ypos, lines);
			ypos += linecount; lines -= linecount;
		}

		subline = 0;
                line = line->next;
	}

	if (fill_bottom) {
		/* clear the rest of the view */
		term_set_color(view->window, ATTR_RESET);
		while (lines > 0) {
			term_move(view->window, 0, ypos);
			term_window_clrtoeol(view->window, ypos);
			ypos++; lines--;
		}
	}
}

#define view_draw_top(view, lines, fill_bottom) \
	view_draw(view, (view)->startline, (view)->subline, \
		  0, lines, fill_bottom)

static void view_draw_bottom(TEXT_BUFFER_VIEW_REC *view, int lines)
{
	LINE_REC *line;
	int ypos, maxline, subline, linecount;

	maxline = view->height-lines;
	line = view->startline; ypos = -view->subline; subline = 0;
	while (line != NULL && ypos < maxline) {
                linecount = view_get_linecount(view, line);
		ypos += linecount;
		if (ypos > maxline) {
			subline = maxline-(ypos-linecount);
			break;
		}
                line = line->next;
	}

        view_draw(view, line, subline, maxline, lines, TRUE);
}

/* lines: this pointer is scrolled by scrollcount screen lines
   subline: this pointer contains the subline position
   scrollcount: the number of lines to scroll down (negative: up)
   draw_nonclean: whether to redraw the screen now

   Returns number of lines actually scrolled */
static int view_scroll(TEXT_BUFFER_VIEW_REC *view, LINE_REC **lines,
		       int *subline, int scrollcount, int draw_nonclean)
{
	int linecount, realcount, scroll_visible;

	if (*lines == NULL)
		return 0;

	/* scroll down */
	scroll_visible = lines == &view->startline;

	realcount = -*subline;
	scrollcount += *subline;
	*subline = 0;
	while (scrollcount > 0) {
		linecount = view_get_linecount(view, *lines);

		if ((scroll_visible && *lines == view->bottom_startline) &&
		    (scrollcount >= view->bottom_subline)) {
			*subline = view->bottom_subline;
			realcount += view->bottom_subline;
			scrollcount = 0;
			break;
		}

		realcount += linecount;
		scrollcount -= linecount;
		if (scrollcount < 0) {
			realcount += scrollcount;
			*subline = linecount+scrollcount;
			scrollcount = 0;
			break;
		}

		if ((*lines)->next == NULL)
			break;

		*lines = (*lines)->next;
	}

	/* scroll up */
	while (scrollcount < 0 && (*lines)->prev != NULL) {
		*lines = (*lines)->prev;
		linecount = view_get_linecount(view, *lines);

		realcount -= linecount;
		scrollcount += linecount;
		if (scrollcount > 0) {
			realcount += scrollcount;
			*subline = scrollcount;
			break;
		}
	}

	if (scroll_visible && realcount != 0 && view->window != NULL) {
		if (realcount <= -view->height || realcount >= view->height) {
			/* scrolled more than screenful, redraw the
			   whole view */
			textbuffer_view_redraw(view);
		} else {
			if (view->width == term_width) {
				/* we can try to use vt100 scroll regions */
				term_set_color(view->window, ATTR_RESET);
				term_window_scroll(view->window, realcount);

				if (draw_nonclean) {
					if (realcount < 0)
						view_draw_top(view, -realcount, TRUE);
					else
						view_draw_bottom(view, realcount);
				}

				term_refresh(view->window);
			} else {
				/* do not bother with vt400 scroll
				   rectangles for now, redraw the
				   whole view */
				view->dirty = TRUE;
				irssi_set_dirty();
			}
		}
	}

	return realcount >= 0 ? realcount : -realcount;
}

/* Resize the view. */
void textbuffer_view_resize(TEXT_BUFFER_VIEW_REC *view, int width, int height)
{
	int linecount;

        g_return_if_fail(view != NULL);
        g_return_if_fail(width > 0);

	if (view->width != width) {
                /* line cache needs to be recreated */
		textbuffer_cache_unref(view->cache);
		view->cache = textbuffer_cache_get(view->siblings, width);
	}

	view->width = width > 10 ? width : 10;
	view->height = height > 1 ? height : 1;

	if (view->buffer->first_line == NULL) {
                view->empty_linecount = height;
		return;
	}

	textbuffer_view_init_bottom(view);

	/* check that we didn't scroll lower than bottom startline.. */
	if (textbuffer_line_exists_after(view->bottom_startline->next,
					 view->startline)) {
		view->startline = view->bottom_startline;
                view->subline = view->bottom_subline;
	} else if (view->startline == view->bottom_startline &&
		   view->subline > view->bottom_subline) {
                view->subline = view->bottom_subline;
	} else if (view->startline != NULL) {
		/* make sure the subline is still in allowed range */
		linecount = view_get_linecount(view, view->startline);
		if (view->subline > linecount)
                        view->subline = linecount;
	} else {
		/* we don't have a startline. still under construction? */
		view->subline = 0;
	}

	textbuffer_view_init_ypos(view);
	if (view->bottom && !view_is_bottom(view)) {
		/* we scrolled to far up, need to get down. go right over
		   the empty lines if there's any */
		view->startline = view->bottom_startline;
		view->subline = view->bottom_subline;
		if (view->empty_linecount > 0) {
			view_scroll(view, &view->startline, &view->subline,
				    -view->empty_linecount, FALSE);
		}
		textbuffer_view_init_ypos(view);
	}

	view->bottom = view_is_bottom(view);
	if (view->bottom) {
		/* check if we left empty space at the bottom.. */
		linecount = view_get_linecount_all(view, view->startline) -
			view->subline;
                if (view->empty_linecount < view->height-linecount)
			view->empty_linecount = view->height-linecount;
                view->more_text = FALSE;
	}

	view->dirty = TRUE;
}

/* Clear the view, don't actually remove any lines from buffer. */
void textbuffer_view_clear(TEXT_BUFFER_VIEW_REC *view)
{
        g_return_if_fail(view != NULL);

	view->ypos = -1;
	view->bottom_startline = view->startline =
		textbuffer_line_last(view->buffer);
	view->bottom_subline = view->subline =
		view->buffer->cur_line == NULL ? 0 :
		view_get_linecount(view, view->buffer->cur_line);
	view->empty_linecount = view->height;
	view->bottom = TRUE;
	view->more_text = FALSE;

        textbuffer_view_redraw(view);
}

/* Scroll the view up/down */
void textbuffer_view_scroll(TEXT_BUFFER_VIEW_REC *view, int lines)
{
	int count;

        g_return_if_fail(view != NULL);

	count = view_scroll(view, &view->startline, &view->subline,
			    lines, TRUE);
	view->ypos += lines < 0 ? count : -count;
	view->bottom = view_is_bottom(view);
        if (view->bottom) view->more_text = FALSE;

        if (view->window != NULL)
		term_refresh(view->window);
}

/* Scroll to specified line */
void textbuffer_view_scroll_line(TEXT_BUFFER_VIEW_REC *view, LINE_REC *line)
{
        g_return_if_fail(view != NULL);

	if (textbuffer_line_exists_after(view->bottom_startline->next, line)) {
		view->startline = view->bottom_startline;
		view->subline = view->bottom_subline;
	} else {
		view->startline = line;
                view->subline = 0;
	}

	textbuffer_view_init_ypos(view);
	view->bottom = view_is_bottom(view);
        if (view->bottom) view->more_text = FALSE;

	textbuffer_view_redraw(view);
}

/* Return line cache */
LINE_CACHE_REC *textbuffer_view_get_line_cache(TEXT_BUFFER_VIEW_REC *view,
					       LINE_REC *line)
{
	LINE_CACHE_REC *cache;

        g_assert(view != NULL);
        g_assert(line != NULL);

	cache = g_hash_table_lookup(view->cache->line_cache, line);
	if (cache == NULL)
		cache = view_update_line_cache(view, line);
        else
		cache->last_access = time(NULL);

        return cache;
}

static void view_insert_line(TEXT_BUFFER_VIEW_REC *view, LINE_REC *line)
{
	int linecount, ypos, subline;

        if (!view->bottom)
		view->more_text = TRUE;

	if (view->bottom_startline == NULL) {
		view->startline = view->bottom_startline =
			view->buffer->first_line;
	}

	if (view->buffer->cur_line != line &&
	    !textbuffer_line_exists_after(view->bottom_startline, line))
		return;

	linecount = view_get_linecount(view, line);
	view->ypos += linecount;
	if (view->empty_linecount > 0) {
		view->empty_linecount -= linecount;
		if (view->empty_linecount >= 0)
			linecount = 0;
		else {
			linecount = -view->empty_linecount;
			view->empty_linecount = 0;
		}
	}

	if (linecount > 0) {
		view_scroll(view, &view->bottom_startline,
			    &view->bottom_subline, linecount, FALSE);
	}

	if (view->bottom) {
		if (view->scroll && view->ypos >= view->height) {
			linecount = view->ypos-view->height+1;
			view_scroll(view, &view->startline,
				    &view->subline, linecount, FALSE);
			view->ypos -= linecount;
		} else {
			view->bottom = view_is_bottom(view);
		}

		if (view->window != NULL && !view_line_is_hidden(view, line)) {
			ypos = view->ypos+1 - view_get_linecount(view, line);
			if (ypos >= 0)
				subline = 0;
			else {
				subline = -ypos;
				ypos = 0;
			}
			if (ypos < view->height) {
				view_line_draw(view, line, subline, ypos,
					       view->height - ypos);
			}
		}
	}

        if (view->window != NULL && !view_line_is_hidden(view, line))
		term_refresh(view->window);
}

/* Update some line in the buffer which has been modified using
   textbuffer_append() or textbuffer_insert(). */
void textbuffer_view_insert_line(TEXT_BUFFER_VIEW_REC *view, LINE_REC *line)
{
        GSList *tmp;
	unsigned char update_counter;

	g_return_if_fail(view != NULL);
	g_return_if_fail(line != NULL);

	if (!view->buffer->last_eol)
                return;

        update_counter = view->cache->update_counter+1;
	view_update_cache(view, line, update_counter);
        view_insert_line(view, line);

	for (tmp = view->siblings; tmp != NULL; tmp = tmp->next) {
		TEXT_BUFFER_VIEW_REC *rec = tmp->data;

                view_update_cache(rec, line, update_counter);
		view_insert_line(rec, line);
	}
}

typedef struct {
	LINE_REC *remove_line;
        GSList *remove_list;
} BOOKMARK_FIND_REC;

static void bookmark_check_remove(char *key, LINE_REC *line,
				  BOOKMARK_FIND_REC *rec)
{
	if (line == rec->remove_line)
                rec->remove_list = g_slist_append(rec->remove_list, key);
}

static void view_bookmarks_check(TEXT_BUFFER_VIEW_REC *view, LINE_REC *line)
{
        BOOKMARK_FIND_REC rec;
	GSList *tmp;

        rec.remove_line = line;
        rec.remove_list = NULL;
	g_hash_table_foreach(view->bookmarks,
			     (GHFunc) bookmark_check_remove, &rec);

	if (rec.remove_list != NULL) {
		for (tmp = rec.remove_list; tmp != NULL; tmp = tmp->next) {
			g_hash_table_remove(view->bookmarks, tmp->data);
			g_free(tmp->data);
		}
		g_slist_free(rec.remove_list);
	}
}

/* Return number of real lines `lines' list takes -
   stops counting when the height reaches the view height */
static int view_get_lines_height(TEXT_BUFFER_VIEW_REC *view,
				 LINE_REC *line, int subline,
				 LINE_REC *skip_line)
{
	int height, linecount;

        height = -subline;
	while (line != NULL && height < view->height) {
		if (line != skip_line) {
                        linecount = view_get_linecount(view, line);
			height += linecount;
		}
                line = line->next;
	}

	return height < view->height ? height : view->height;
}

/* line: line to remove
   linecount: linecount of that line, to be offset when the line was in/below view

   scroll the window maintaining the startline while removing line
   if startline is removed, make the previous line the new startline
*/
static void view_remove_line_update_startline(TEXT_BUFFER_VIEW_REC *view,
					      LINE_REC *line, int linecount)
{
	int scroll;

	if (view->startline == line) {
		view->startline = view->startline->prev != NULL ?
			view->startline->prev : view->startline->next;
		view->subline = 0;
	} else {
		scroll = view->height -
			view_get_lines_height(view, view->startline,
					      view->subline, line);
		if (scroll > 0) {
			view_scroll(view, &view->startline,
				    &view->subline, -scroll, FALSE);
		}
	}

	/* FIXME: this is slow and unnecessary, but it's easy and
	   really works :) */
	textbuffer_view_init_ypos(view);
	if (textbuffer_line_exists_after(view->startline, line))
		view->ypos -= linecount;
}

static void view_remove_line(TEXT_BUFFER_VIEW_REC *view, LINE_REC *line,
			     int linecount)
{
	int realcount;

	view_bookmarks_check(view, line);

	if (view->buffer->cur_line == line) {
                /* the last line is being removed */
		LINE_REC *prevline;

		prevline = view->buffer->first_line == line ? NULL :
			textbuffer_line_last(view->buffer)->prev;
		view->cache->last_linecount = prevline == NULL ? 0 :
			view_get_linecount(view, prevline);
	}

	/* first line in the buffer - this is the most commonly
	   removed line.. */
	if (view->buffer->first_line == line) {
		if (view->bottom_startline == line) {
			/* very small scrollback.. */
                        view->bottom_startline = view->bottom_startline->next;
			view->bottom_subline = 0;
		}

		if (view->startline == line) {
                        /* removing the first line in screen */
			int is_last = view->startline->next == NULL;

			realcount = view_scroll(view, &view->startline,
						&view->subline,
						linecount, FALSE);
			view->ypos -= realcount;
			view->empty_linecount += linecount-realcount;
			if (is_last == 1)
				view->startline = NULL;
		}

		if (view->startline == line) {
			view->startline = line->next;
			view->subline = 0;
		}
	} else {
		if (textbuffer_line_exists_after(view->bottom_startline,
						 line)) {
			realcount = view_scroll(view, &view->bottom_startline,
						&view->bottom_subline,
						-linecount, FALSE);
			view->empty_linecount += linecount-realcount;
		}

		if (view->bottom_startline == line) {
			view->bottom_startline = view->bottom_startline->next;
			view->bottom_subline = 0;
		}

		if (textbuffer_line_exists_after(view->startline,
						 line)) {
			view_remove_line_update_startline(view, line,
							  linecount);
		}
	}

	view->bottom = view_is_bottom(view);
        if (view->bottom) view->more_text = FALSE;
        if (view->window != NULL)
		term_refresh(view->window);
}

/* Remove one line from buffer. */
void textbuffer_view_remove_line(TEXT_BUFFER_VIEW_REC *view, LINE_REC *line)
{
        GSList *tmp;
	unsigned char update_counter;
        int linecount;

	g_return_if_fail(view != NULL);
	g_return_if_fail(line != NULL);

	signal_emit("gui textbuffer line removed", 3, view, line, line->prev);

        linecount = view_get_linecount(view, line);
        update_counter = view->cache->update_counter+1;

        view_remove_line(view, line, linecount);
	view_remove_cache(view, line, update_counter);

	for (tmp = view->siblings; tmp != NULL; tmp = tmp->next) {
		TEXT_BUFFER_VIEW_REC *rec = tmp->data;

		view_remove_line(rec, line, linecount);
		view_remove_cache(rec, line, update_counter);
	}

	textbuffer_remove(view->buffer, line);
	if (view->bottom_startline == NULL) {
		/* We may have removed the bottom_startline, make sure
		   that scroll doesn't get stuck */
		textbuffer_view_init_bottom(view);
	}
}

void textbuffer_view_remove_lines_by_level(TEXT_BUFFER_VIEW_REC *view, int level)
{
	LINE_REC *line, *next;

	term_refresh_freeze();
	line = textbuffer_view_get_lines(view);

	while (line != NULL) {
		next = line->next;

		if (line->info.level & level)
			textbuffer_view_remove_line(view, line);
		line = next;
	}
	textbuffer_view_redraw(view);
	term_refresh_thaw();
}

/* Remove all lines from buffer. */
void textbuffer_view_remove_all_lines(TEXT_BUFFER_VIEW_REC *view)
{
	g_return_if_fail(view != NULL);

	textbuffer_remove_all_lines(view->buffer);

	g_hash_table_foreach(view->bookmarks, (GHFunc) g_free, NULL);
	g_hash_table_remove_all(view->bookmarks);

	textbuffer_view_reset_cache(view);
	textbuffer_view_clear(view);
	g_slist_foreach(view->siblings, (GFunc) textbuffer_view_clear, NULL);
}

/* Set a bookmark in view */
void textbuffer_view_set_bookmark(TEXT_BUFFER_VIEW_REC *view,
				  const char *name, LINE_REC *line)
{
	gpointer key, value;

	g_return_if_fail(view != NULL);
	g_return_if_fail(name != NULL);

	if (g_hash_table_lookup_extended(view->bookmarks, name,
					 &key, &value)) {
		g_hash_table_remove(view->bookmarks, key);
                g_free(key);
	}

	g_hash_table_insert(view->bookmarks, g_strdup(name), line);
}

/* Set a bookmark in view to the bottom line */
void textbuffer_view_set_bookmark_bottom(TEXT_BUFFER_VIEW_REC *view,
					 const char *name)
{
	LINE_REC *line;

	g_return_if_fail(view != NULL);
	g_return_if_fail(name != NULL);

	if (view->bottom_startline != NULL) {
                line = textbuffer_line_last(view->buffer);
		textbuffer_view_set_bookmark(view, name, line);
	}
}

/* Return the line for bookmark */
LINE_REC *textbuffer_view_get_bookmark(TEXT_BUFFER_VIEW_REC *view,
				       const char *name)
{
	g_return_val_if_fail(view != NULL, NULL);
	g_return_val_if_fail(name != NULL, NULL);

        return g_hash_table_lookup(view->bookmarks, name);
}

void textbuffer_view_set_hidden_level(TEXT_BUFFER_VIEW_REC *view, int level)
{
	g_return_if_fail(view != NULL);

	if (view->hidden_level != level) {
		if (view->empty_linecount > 0 && view->startline != NULL) {
			int old_height, new_height;
			LINE_REC *hidden_start;

			hidden_start = view->startline;
			while (hidden_start->prev != NULL && view_line_is_hidden(view, hidden_start->prev)) {
				hidden_start = hidden_start->prev;
			}

			old_height = view_get_lines_height(view, hidden_start, view->subline, NULL);
			view->hidden_level = level;
			new_height = view_get_lines_height(view, hidden_start, view->subline, NULL);

			view->empty_linecount -= new_height - old_height;

			if (view->empty_linecount < 0)
				view->empty_linecount = 0;
			else if (view->empty_linecount > view->height)
				view->empty_linecount = view->height;
		} else {
			view->hidden_level = level;
		}
		textbuffer_view_resize(view, view->width, view->height);
	}
}

/* Specify window where the changes in view should be drawn,
   NULL disables it. */
void textbuffer_view_set_window(TEXT_BUFFER_VIEW_REC *view,
				TERM_WINDOW *window)
{
	g_return_if_fail(view != NULL);

	if (view->window != window) {
		view->window = window;
                if (window != NULL)
			view->dirty = TRUE;
	}
}

/* Redraw a view to window */
void textbuffer_view_redraw(TEXT_BUFFER_VIEW_REC *view)
{
	g_return_if_fail(view != NULL);

	if (view->window != NULL) {
		view->dirty = FALSE;
		view_draw_top(view, view->height, TRUE);
		term_refresh(view->window);
	}
}

static int line_cache_check_remove(void *key, LINE_CACHE_REC *cache,
				   time_t *now)
{
	if (cache->last_access+LINE_CACHE_KEEP_TIME > *now)
		return FALSE;

	line_cache_destroy(NULL, cache);
	return TRUE;
}

static int sig_check_linecache(void)
{
	GSList *tmp, *caches;
        time_t now;

        now = time(NULL); caches = NULL;
	for (tmp = views; tmp != NULL; tmp = tmp->next) {
		TEXT_BUFFER_VIEW_REC *rec = tmp->data;

		if (g_slist_find(caches, rec->cache) != NULL)
			continue;

		caches = g_slist_append(caches, rec->cache);
		g_hash_table_foreach_remove(rec->cache->line_cache,
					    (GHRFunc) line_cache_check_remove,
					    &now);
	}

        g_slist_free(caches);
	return 1;
}

void textbuffer_view_init(void)
{
	linecache_tag = g_timeout_add(LINE_CACHE_CHECK_TIME, (GSourceFunc) sig_check_linecache, NULL);
}

void textbuffer_view_deinit(void)
{
	g_source_remove(linecache_tag);
}
