/*-
 * Copyright (c) 1993, 1994
 *	The Regents of the University of California.  All rights reserved.
 * Copyright (c) 1993, 1994, 1995, 1996
 *	Keith Bostic.  All rights reserved.
 *
 * See the LICENSE file for redistribution information.
 */

#include "config.h"

#ifndef lint
static const char sccsid[] = "@(#)vs_smap.c	10.25 (Berkeley) 7/12/96";
#endif /* not lint */

#include <sys/types.h>
#include <sys/queue.h>
#include <sys/time.h>

#include <bitstring.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "../common/common.h"
#include "vi.h"

static int	vs_deleteln __P((SCR *, int));
static int	vs_insertln __P((SCR *, int));
static int	vs_sm_delete __P((SCR *, recno_t));
static int	vs_sm_down __P((SCR *, MARK *, recno_t, scroll_t, SMAP *));
static int	vs_sm_erase __P((SCR *));
static int	vs_sm_insert __P((SCR *, recno_t));
static int	vs_sm_reset __P((SCR *, recno_t));
static int	vs_sm_up __P((SCR *, MARK *, recno_t, scroll_t, SMAP *));

/*
 * vs_change --
 *	Make a change to the screen.
 *
 * PUBLIC: int vs_change __P((SCR *, recno_t, lnop_t));
 */
int
vs_change(sp, lno, op)
	SCR *sp;
	recno_t lno;
	lnop_t op;
{
	VI_PRIVATE *vip;
	SMAP *p;
	size_t cnt, oldy, oldx;

	vip = VIP(sp);

	/*
	 * XXX
	 * Very nasty special case.  The historic vi code displays a single
	 * space (or a '$' if the list option is set) for the first line in
	 * an "empty" file.  If we "insert" a line, that line gets scrolled
	 * down, not repainted, so it's incorrect when we refresh the screen.
	 * The vi text input functions detect it explicitly and don't insert
	 * a new line.
	 *
	 * Check for line #2 before going to the end of the file.
	 */
	if ((op == LINE_APPEND && lno == 0 || op == LINE_INSERT && lno == 1) &&
	    !db_exist(sp, 2)) {
		lno = 1;
		op = LINE_RESET;
	}

	/* Appending is the same as inserting, if the line is incremented. */
	if (op == LINE_APPEND) {
		++lno;
		op = LINE_INSERT;
	}

	/* Ignore the change if the line is after the map. */
	if (lno > TMAP->lno)
		return (0);

	/*
	 * If the line is before the map, and it's a decrement, decrement
	 * the map.  If it's an increment, increment the map.  Otherwise,
	 * ignore it.
	 */
	if (lno < HMAP->lno) {
		switch (op) {
		case LINE_APPEND:
			abort();
			/* NOTREACHED */
		case LINE_DELETE:
			for (p = HMAP, cnt = sp->t_rows; cnt--; ++p)
				--p->lno;
			if (sp->lno >= lno)
				--sp->lno;
			F_SET(vip, VIP_N_RENUMBER);
			break;
		case LINE_INSERT:
			for (p = HMAP, cnt = sp->t_rows; cnt--; ++p)
				++p->lno;
			if (sp->lno >= lno)
				++sp->lno;
			F_SET(vip, VIP_N_RENUMBER);
			break;
		case LINE_RESET:
			break;
		}
		return (0);
	}

	F_SET(vip, VIP_N_REFRESH);

	/*
	 * Invalidate the line size cache, and invalidate the cursor if it's
	 * on this line,
	 */
	VI_SCR_CFLUSH(vip);
	if (sp->lno == lno)
		F_SET(vip, VIP_CUR_INVALID);

	/*
	 * If ex modifies the screen after ex output is already on the screen
	 * or if we've switched into ex canonical mode, don't touch it -- we'll
	 * get scrolling wrong, at best.
	 */
	if (!F_ISSET(sp, SC_TINPUT_INFO) &&
	    (F_ISSET(sp, SC_SCR_EXWROTE) || VIP(sp)->totalcount > 1)) {
		F_SET(vip, VIP_N_EX_REDRAW);
		return (0);
	}

	/* Save and restore the cursor for these routines. */
	(void)sp->gp->scr_cursor(sp, &oldy, &oldx);

	switch (op) {
	case LINE_DELETE:
		if (vs_sm_delete(sp, lno))
			return (1);
		F_SET(vip, VIP_N_RENUMBER);
		break;
	case LINE_INSERT:
		if (vs_sm_insert(sp, lno))
			return (1);
		F_SET(vip, VIP_N_RENUMBER);
		break;
	case LINE_RESET:
		if (vs_sm_reset(sp, lno))
			return (1);
		break;
	default:
		abort();
	}

	(void)sp->gp->scr_move(sp, oldy, oldx);
	return (0);
}

/*
 * vs_sm_fill --
 *	Fill in the screen map, placing the specified line at the
 *	right position.  There isn't any way to tell if an SMAP
 *	entry has been filled in, so this routine had better be
 *	called with P_FILL set before anything else is done.
 *
 * !!!
 * Unexported interface: if lno is OOBLNO, P_TOP means that the HMAP
 * slot is already filled in, P_BOTTOM means that the TMAP slot is
 * already filled in, and we just finish up the job.
 *
 * PUBLIC: int vs_sm_fill __P((SCR *, recno_t, pos_t));
 */
int
vs_sm_fill(sp, lno, pos)
	SCR *sp;
	recno_t lno;
	pos_t pos;
{
	SMAP *p, tmp;
	size_t cnt;

	/* Flush all cached information from the SMAP. */
	for (p = HMAP, cnt = sp->t_rows; cnt--; ++p)
		SMAP_FLUSH(p);

	/*
	 * If the map is filled, the screen must be redrawn.
	 *
	 * XXX
	 * This is a bug.  We should try and figure out if the desired line
	 * is already in the map or close by -- scrolling the screen would
	 * be a lot better than redrawing.
	 */
	F_SET(sp, SC_SCR_REDRAW);

	switch (pos) {
	case P_FILL:
		tmp.lno = 1;
		tmp.coff = 0;
		tmp.soff = 1;

		/* See if less than half a screen from the top. */
		if (vs_sm_nlines(sp,
		    &tmp, lno, HALFTEXT(sp)) <= HALFTEXT(sp)) {
			lno = 1;
			goto top;
		}

		/* See if less than half a screen from the bottom. */
		if (db_last(sp, &tmp.lno))
			return (1);
		tmp.coff = 0;
		tmp.soff = vs_screens(sp, tmp.lno, NULL);
		if (vs_sm_nlines(sp,
		    &tmp, lno, HALFTEXT(sp)) <= HALFTEXT(sp)) {
			TMAP->lno = tmp.lno;
			TMAP->coff = tmp.coff;
			TMAP->soff = tmp.soff;
			goto bottom;
		}
		goto middle;
	case P_TOP:
		if (lno != OOBLNO) {
top:			HMAP->lno = lno;
			HMAP->coff = 0;
			HMAP->soff = 1;
		}
		/* If we fail, just punt. */
		for (p = HMAP, cnt = sp->t_rows; --cnt; ++p)
			if (vs_sm_next(sp, p, p + 1))
				goto err;
		break;
	case P_MIDDLE:
		/* If we fail, guess that the file is too small. */
middle:		p = HMAP + sp->t_rows / 2;
		p->lno = lno;
		p->coff = 0;
		p->soff = 1;
		for (; p > HMAP; --p)
			if (vs_sm_prev(sp, p, p - 1)) {
				lno = 1;
				goto top;
			}

		/* If we fail, just punt. */
		p = HMAP + sp->t_rows / 2;
		for (; p < TMAP; ++p)
			if (vs_sm_next(sp, p, p + 1))
				goto err;
		break;
	case P_BOTTOM:
		if (lno != OOBLNO) {
			TMAP->lno = lno;
			TMAP->coff = 0;
			TMAP->soff = vs_screens(sp, lno, NULL);
		}
		/* If we fail, guess that the file is too small. */
bottom:		for (p = TMAP; p > HMAP; --p)
			if (vs_sm_prev(sp, p, p - 1)) {
				lno = 1;
				goto top;
			}
		break;
	default:
		abort();
	}
	return (0);

	/*
	 * Try and put *something* on the screen.  If this fails, we have a
	 * serious hard error.
	 */
err:	HMAP->lno = 1;
	HMAP->coff = 0;
	HMAP->soff = 1;
	for (p = HMAP; p < TMAP; ++p)
		if (vs_sm_next(sp, p, p + 1))
			return (1);
	return (0);
}

/*
 * For the routines vs_sm_reset, vs_sm_delete and vs_sm_insert: if the
 * screen contains only a single line (whether because the screen is small
 * or the line large), it gets fairly exciting.  Skip the fun, set a flag
 * so the screen map is refilled and the screen redrawn, and return.  This
 * is amazingly slow, but it's not clear that anyone will care.
 */
#define	HANDLE_WEIRDNESS(cnt) {						\
	if (cnt >= sp->t_rows) {					\
		F_SET(sp, SC_SCR_REFORMAT);				\
		return (0);						\
	}								\
}

/*
 * vs_sm_delete --
 *	Delete a line out of the SMAP.
 */
static int
vs_sm_delete(sp, lno)
	SCR *sp;
	recno_t lno;
{
	SMAP *p, *t;
	size_t cnt_orig;

	/*
	 * Find the line in the map, and count the number of screen lines
	 * which display any part of the deleted line.
	 */
	for (p = HMAP; p->lno != lno; ++p);
	if (O_ISSET(sp, O_LEFTRIGHT))
		cnt_orig = 1;
	else
		for (cnt_orig = 1, t = p + 1;
		    t <= TMAP && t->lno == lno; ++cnt_orig, ++t);

	HANDLE_WEIRDNESS(cnt_orig);

	/* Delete that many lines from the screen. */
	(void)sp->gp->scr_move(sp, p - HMAP, 0);
	if (vs_deleteln(sp, cnt_orig))
		return (1);

	/* Shift the screen map up. */
	memmove(p, p + cnt_orig, (((TMAP - p) - cnt_orig) + 1) * sizeof(SMAP));

	/* Decrement the line numbers for the rest of the map. */
	for (t = TMAP - cnt_orig; p <= t; ++p)
		--p->lno;

	/* Display the new lines. */
	for (p = TMAP - cnt_orig;;) {
		if (p < TMAP && vs_sm_next(sp, p, p + 1))
			return (1);
		/* vs_sm_next() flushed the cache. */
		if (vs_line(sp, ++p, NULL, NULL))
			return (1);
		if (p == TMAP)
			break;
	}
	return (0);
}

/*
 * vs_sm_insert --
 *	Insert a line into the SMAP.
 */
static int
vs_sm_insert(sp, lno)
	SCR *sp;
	recno_t lno;
{
	SMAP *p, *t;
	size_t cnt_orig, cnt, coff;

	/* Save the offset. */
	coff = HMAP->coff;

	/*
	 * Find the line in the map, find out how many screen lines
	 * needed to display the line.
	 */
	for (p = HMAP; p->lno != lno; ++p);

	cnt_orig = vs_screens(sp, lno, NULL);
	HANDLE_WEIRDNESS(cnt_orig);

	/*
	 * The lines left in the screen override the number of screen
	 * lines in the inserted line.
	 */
	cnt = (TMAP - p) + 1;
	if (cnt_orig > cnt)
		cnt_orig = cnt;

	/* Push down that many lines. */
	(void)sp->gp->scr_move(sp, p - HMAP, 0);
	if (vs_insertln(sp, cnt_orig))
		return (1);

	/* Shift the screen map down. */
	memmove(p + cnt_orig, p, (((TMAP - p) - cnt_orig) + 1) * sizeof(SMAP));

	/* Increment the line numbers for the rest of the map. */
	for (t = p + cnt_orig; t <= TMAP; ++t)
		++t->lno;

	/* Fill in the SMAP for the new lines, and display. */
	for (cnt = 1, t = p; cnt <= cnt_orig; ++t, ++cnt) {
		t->lno = lno;
		t->coff = coff;
		t->soff = cnt;
		SMAP_FLUSH(t);
		if (vs_line(sp, t, NULL, NULL))
			return (1);
	}
	return (0);
}

/*
 * vs_sm_reset --
 *	Reset a line in the SMAP.
 */
static int
vs_sm_reset(sp, lno)
	SCR *sp;
	recno_t lno;
{
	SMAP *p, *t;
	size_t cnt_orig, cnt_new, cnt, diff;

	/*
	 * See if the number of on-screen rows taken up by the old display
	 * for the line is the same as the number needed for the new one.
	 * If so, repaint, otherwise do it the hard way.
	 */
	for (p = HMAP; p->lno != lno; ++p);
	if (O_ISSET(sp, O_LEFTRIGHT)) {
		t = p;
		cnt_orig = cnt_new = 1;
	} else {
		for (cnt_orig = 0,
		    t = p; t <= TMAP && t->lno == lno; ++cnt_orig, ++t);
		cnt_new = vs_screens(sp, lno, NULL);
	}

	HANDLE_WEIRDNESS(cnt_orig);

	if (cnt_orig == cnt_new) {
		do {
			SMAP_FLUSH(p);
			if (vs_line(sp, p, NULL, NULL))
				return (1);
		} while (++p < t);
		return (0);
	}

	if (cnt_orig < cnt_new) {
		/* Get the difference. */
		diff = cnt_new - cnt_orig;

		/*
		 * The lines left in the screen override the number of screen
		 * lines in the inserted line.
		 */
		cnt = (TMAP - p) + 1;
		if (diff > cnt)
			diff = cnt;

		/* If there are any following lines, push them down. */
		if (cnt > 1) {
			(void)sp->gp->scr_move(sp, p - HMAP, 0);
			if (vs_insertln(sp, diff))
				return (1);

			/* Shift the screen map down. */
			memmove(p + diff, p,
			    (((TMAP - p) - diff) + 1) * sizeof(SMAP));
		}

		/* Fill in the SMAP for the replaced line, and display. */
		for (cnt = 1, t = p; cnt_new-- && t <= TMAP; ++t, ++cnt) {
			t->lno = lno;
			t->soff = cnt;
			SMAP_FLUSH(t);
			if (vs_line(sp, t, NULL, NULL))
				return (1);
		}
	} else {
		/* Get the difference. */
		diff = cnt_orig - cnt_new;

		/* Delete that many lines from the screen. */
		(void)sp->gp->scr_move(sp, p - HMAP, 0);
		if (vs_deleteln(sp, diff))
			return (1);

		/* Shift the screen map up. */
		memmove(p, p + diff, (((TMAP - p) - diff) + 1) * sizeof(SMAP));

		/* Fill in the SMAP for the replaced line, and display. */
		for (cnt = 1, t = p; cnt_new--; ++t, ++cnt) {
			t->lno = lno;
			t->soff = cnt;
			SMAP_FLUSH(t);
			if (vs_line(sp, t, NULL, NULL))
				return (1);
		}

		/* Display the new lines at the bottom of the screen. */
		for (t = TMAP - diff;;) {
			if (t < TMAP && vs_sm_next(sp, t, t + 1))
				return (1);
			/* vs_sm_next() flushed the cache. */
			if (vs_line(sp, ++t, NULL, NULL))
				return (1);
			if (t == TMAP)
				break;
		}
	}
	return (0);
}

/*
 * vs_sm_scroll
 *	Scroll the SMAP up/down count logical lines.  Different
 *	semantics based on the vi command, *sigh*.
 *
 * PUBLIC: int vs_sm_scroll __P((SCR *, MARK *, recno_t, scroll_t));
 */
int
vs_sm_scroll(sp, rp, count, scmd)
	SCR *sp;
	MARK *rp;
	recno_t count;
	scroll_t scmd;
{
	SMAP *smp;

	/*
	 * Invalidate the cursor.  The line is probably going to change,
	 * (although for ^E and ^Y it may not).  In any case, the scroll
	 * routines move the cursor to draw things.
	 */
	F_SET(VIP(sp), VIP_CUR_INVALID);

	/* Find the cursor in the screen. */
	if (vs_sm_cursor(sp, &smp))
		return (1);

	switch (scmd) {
	case CNTRL_B:
	case CNTRL_U:
	case CNTRL_Y:
	case Z_CARAT:
		if (vs_sm_down(sp, rp, count, scmd, smp))
			return (1);
		break;
	case CNTRL_D:
	case CNTRL_E:
	case CNTRL_F:
	case Z_PLUS:
		if (vs_sm_up(sp, rp, count, scmd, smp))
			return (1);
		break;
	default:
		abort();
	}

	/*
	 * !!!
	 * If we're at the start of a line, go for the first non-blank.
	 * This makes it look like the old vi, even though we're moving
	 * around by logical lines, not physical ones.
	 *
	 * XXX
	 * In the presence of a long line, which has more than a screen
	 * width of leading spaces, this code can cause a cursor warp.
	 * Live with it.
	 */
	if (scmd != CNTRL_E && scmd != CNTRL_Y &&
	    rp->cno == 0 && nonblank(sp, rp->lno, &rp->cno))
		return (1);

	return (0);
}

/*
 * vs_sm_up --
 *	Scroll the SMAP up count logical lines.
 */
static int
vs_sm_up(sp, rp, count, scmd, smp)
	SCR *sp;
	MARK *rp;
	scroll_t scmd;
	recno_t count;
	SMAP *smp;
{
	int cursor_set, echanged, zset;
	SMAP *ssmp, s1, s2;

	/*
	 * Check to see if movement is possible.
	 *
	 * Get the line after the map.  If that line is a new one (and if
	 * O_LEFTRIGHT option is set, this has to be true), and the next
	 * line doesn't exist, and the cursor doesn't move, or the cursor
	 * isn't even on the screen, or the cursor is already at the last
	 * line in the map, it's an error.  If that test succeeded because
	 * the cursor wasn't at the end of the map, test to see if the map
	 * is mostly empty.
	 */
	if (vs_sm_next(sp, TMAP, &s1))
		return (1);
	if (s1.lno > TMAP->lno && !db_exist(sp, s1.lno)) {
		if (scmd == CNTRL_E || scmd == Z_PLUS || smp == TMAP) {
			v_eof(sp, NULL);
			return (1);
		}
		if (vs_sm_next(sp, smp, &s1))
			return (1);
		if (s1.lno > smp->lno && !db_exist(sp, s1.lno)) {
			v_eof(sp, NULL);
			return (1);
		}
	}

	/*
	 * Small screens: see vs_refresh.c section 6a.
	 *
	 * If it's a small screen, and the movement isn't larger than a
	 * screen, i.e some context will remain, open up the screen and
	 * display by scrolling.  In this case, the cursor moves down one
	 * line for each line displayed.  Otherwise, erase/compress and
	 * repaint, and move the cursor to the first line in the screen.
	 * Note, the ^F command is always in the latter case, for historical
	 * reasons.
	 */
	cursor_set = 0;
	if (IS_SMALL(sp)) {
		if (count >= sp->t_maxrows || scmd == CNTRL_F) {
			s1 = TMAP[0];
			if (vs_sm_erase(sp))
				return (1);
			for (; count--; s1 = s2) {
				if (vs_sm_next(sp, &s1, &s2))
					return (1);
				if (s2.lno != s1.lno && !db_exist(sp, s2.lno))
					break;
			}
			TMAP[0] = s2;
			if (vs_sm_fill(sp, OOBLNO, P_BOTTOM))
				return (1);
			return (vs_sm_position(sp, rp, 0, P_TOP));
		}
		cursor_set = scmd == CNTRL_E || vs_sm_cursor(sp, &ssmp);
		for (; count &&
		    sp->t_rows != sp->t_maxrows; --count, ++sp->t_rows) {
			if (vs_sm_next(sp, TMAP, &s1))
				return (1);
			if (TMAP->lno != s1.lno && !db_exist(sp, s1.lno))
				break;
			*++TMAP = s1;
			/* vs_sm_next() flushed the cache. */
			if (vs_line(sp, TMAP, NULL, NULL))
				return (1);

			if (!cursor_set)
				++ssmp;
		}
		if (!cursor_set) {
			rp->lno = ssmp->lno;
			rp->cno = ssmp->c_sboff;
		}
		if (count == 0)
			return (0);
	}

	for (echanged = zset = 0; count; --count) {
		/* Decide what would show up on the screen. */
		if (vs_sm_next(sp, TMAP, &s1))
			return (1);

		/* If the line doesn't exist, we're done. */
		if (TMAP->lno != s1.lno && !db_exist(sp, s1.lno))
			break;

		/* Scroll the screen cursor up one logical line. */
		if (vs_sm_1up(sp))
			return (1);
		switch (scmd) {
		case CNTRL_E:
			if (smp > HMAP)
				--smp;
			else
				echanged = 1;
			break;
		case Z_PLUS:
			if (zset) {
				if (smp > HMAP)
					--smp;
			} else {
				smp = TMAP;
				zset = 1;
			}
			/* FALLTHROUGH */
		default:
			break;
		}
	}

	if (cursor_set)
		return(0);

	switch (scmd) {
	case CNTRL_E:
		/*
		 * On a ^E that was forced to change lines, try and keep the
		 * cursor as close as possible to the last position, but also
		 * set it up so that the next "real" movement will return the
		 * cursor to the closest position to the last real movement.
		 */
		if (echanged) {
			rp->lno = smp->lno;
			rp->cno = vs_colpos(sp, smp->lno,
			    (O_ISSET(sp, O_LEFTRIGHT) ? 
			    smp->coff : (smp->soff - 1) * sp->cols) +
			    sp->rcm % sp->cols);
		}
		return (0);
	case CNTRL_F:
		/*
		 * If there are more lines, the ^F command is positioned at
		 * the first line of the screen.
		 */
		if (!count) {
			smp = HMAP;
			break;
		}
		/* FALLTHROUGH */
	case CNTRL_D:
		/*
		 * The ^D and ^F commands move the cursor towards EOF
		 * if there are more lines to move.  Check to be sure
		 * the lines actually exist.  (They may not if the
		 * file is smaller than the screen.)
		 */
		for (; count; --count, ++smp)
			if (smp == TMAP || !db_exist(sp, smp[1].lno))
				break;
		break;
	case Z_PLUS:
		 /* The z+ command moves the cursor to the first new line. */
		break;
	default:
		abort();
	}

	if (!SMAP_CACHE(smp) && vs_line(sp, smp, NULL, NULL))
		return (1);
	rp->lno = smp->lno;
	rp->cno = smp->c_sboff;
	return (0);
}

/*
 * vs_sm_1up --
 *	Scroll the SMAP up one.
 *
 * PUBLIC: int vs_sm_1up __P((SCR *));
 */
int
vs_sm_1up(sp)
	SCR *sp;
{
	/*
	 * Delete the top line of the screen.  Shift the screen map
	 * up and display a new line at the bottom of the screen.
	 */
	(void)sp->gp->scr_move(sp, 0, 0);
	if (vs_deleteln(sp, 1))
		return (1);

	/* One-line screens can fail. */
	if (IS_ONELINE(sp)) {
		if (vs_sm_next(sp, TMAP, TMAP))
			return (1);
	} else {
		memmove(HMAP, HMAP + 1, (sp->rows - 1) * sizeof(SMAP));
		if (vs_sm_next(sp, TMAP - 1, TMAP))
			return (1);
	}
	/* vs_sm_next() flushed the cache. */
	return (vs_line(sp, TMAP, NULL, NULL));
}

/*
 * vs_deleteln --
 *	Delete a line a la curses, make sure to put the information
 *	line and other screens back.
 */
static int
vs_deleteln(sp, cnt)
	SCR *sp;
	int cnt;
{
	GS *gp;
	size_t oldy, oldx;

	gp = sp->gp;
	if (IS_ONELINE(sp))
		(void)gp->scr_clrtoeol(sp);
	else {
		(void)gp->scr_cursor(sp, &oldy, &oldx);
		while (cnt--) {
			(void)gp->scr_deleteln(sp);
			(void)gp->scr_move(sp, LASTLINE(sp), 0);
			(void)gp->scr_insertln(sp);
			(void)gp->scr_move(sp, oldy, oldx);
		}
	}
	return (0);
}

/*
 * vs_sm_down --
 *	Scroll the SMAP down count logical lines.
 */
static int
vs_sm_down(sp, rp, count, scmd, smp)
	SCR *sp;
	MARK *rp;
	recno_t count;
	SMAP *smp;
	scroll_t scmd;
{
	SMAP *ssmp, s1, s2;
	int cursor_set, ychanged, zset;

	/* Check to see if movement is possible. */
	if (HMAP->lno == 1 &&
	    (O_ISSET(sp, O_LEFTRIGHT) || HMAP->soff == 1) &&
	    (scmd == CNTRL_Y || scmd == Z_CARAT || smp == HMAP)) {
		v_sof(sp, NULL);
		return (1);
	}

	/*
	 * Small screens: see vs_refresh.c section 6a.
	 *
	 * If it's a small screen, and the movement isn't larger than a
	 * screen, i.e some context will remain, open up the screen and
	 * display by scrolling.  In this case, the cursor moves up one
	 * line for each line displayed.  Otherwise, erase/compress and
	 * repaint, and move the cursor to the first line in the screen.
	 * Note, the ^B command is always in the latter case, for historical
	 * reasons.
	 */
	cursor_set = scmd == CNTRL_Y;
	if (IS_SMALL(sp)) {
		if (count >= sp->t_maxrows || scmd == CNTRL_B) {
			s1 = HMAP[0];
			if (vs_sm_erase(sp))
				return (1);
			for (; count--; s1 = s2) {
				if (vs_sm_prev(sp, &s1, &s2))
					return (1);
				if (s2.lno == 1 &&
				    (O_ISSET(sp, O_LEFTRIGHT) || s2.soff == 1))
					break;
			}
			HMAP[0] = s2;
			if (vs_sm_fill(sp, OOBLNO, P_TOP))
				return (1);
			return (vs_sm_position(sp, rp, 0, P_BOTTOM));
		}
		cursor_set = scmd == CNTRL_Y || vs_sm_cursor(sp, &ssmp);
		for (; count &&
		    sp->t_rows != sp->t_maxrows; --count, ++sp->t_rows) {
			if (HMAP->lno == 1 &&
			    (O_ISSET(sp, O_LEFTRIGHT) || HMAP->soff == 1))
				break;
			++TMAP;
			if (vs_sm_1down(sp))
				return (1);
		}
		if (!cursor_set) {
			rp->lno = ssmp->lno;
			rp->cno = ssmp->c_sboff;
		}
		if (count == 0)
			return (0);
	}

	for (ychanged = zset = 0; count; --count) {
		/* If the line doesn't exist, we're done. */
		if (HMAP->lno == 1 &&
		    (O_ISSET(sp, O_LEFTRIGHT) || HMAP->soff == 1))
			break;

		/* Scroll the screen and cursor down one logical line. */
		if (vs_sm_1down(sp))
			return (1);
		switch (scmd) {
		case CNTRL_Y:
			if (smp < TMAP)
				++smp;
			else
				ychanged = 1;
			break;
		case Z_CARAT:
			if (zset) {
				if (smp < TMAP)
					++smp;
			} else {
				smp = HMAP;
				zset = 1;
			}
			/* FALLTHROUGH */
		default:
			break;
		}
	}

	if (scmd != CNTRL_Y && cursor_set)
		return(0);

	switch (scmd) {
	case CNTRL_B:
		/*
		 * If there are more lines, the ^B command is positioned at
		 * the last line of the screen.  However, the line may not
		 * exist.
		 */
		if (!count) {
			for (smp = TMAP; smp > HMAP; --smp)
				if (db_exist(sp, smp->lno))
					break;
			break;
		}
		/* FALLTHROUGH */
	case CNTRL_U:
		/*
		 * The ^B and ^U commands move the cursor towards SOF
		 * if there are more lines to move.
		 */
		if (count < smp - HMAP)
			smp -= count;
		else
			smp = HMAP;
		break;
	case CNTRL_Y:
		/*
		 * On a ^Y that was forced to change lines, try and keep the
		 * cursor as close as possible to the last position, but also
		 * set it up so that the next "real" movement will return the
		 * cursor to the closest position to the last real movement.
		 */
		if (ychanged) {
			rp->lno = smp->lno;
			rp->cno = vs_colpos(sp, smp->lno,
			    (O_ISSET(sp, O_LEFTRIGHT) ? 
			    smp->coff : (smp->soff - 1) * sp->cols) +
			    sp->rcm % sp->cols);
		}
		return (0);
	case Z_CARAT:
		 /* The z^ command moves the cursor to the first new line. */
		break;
	default:
		abort();
	}

	if (!SMAP_CACHE(smp) && vs_line(sp, smp, NULL, NULL))
		return (1);
	rp->lno = smp->lno;
	rp->cno = smp->c_sboff;
	return (0);
}

/*
 * vs_sm_erase --
 *	Erase the small screen area for the scrolling functions.
 */
static int
vs_sm_erase(sp)
	SCR *sp;
{
	GS *gp;

	gp = sp->gp;
	(void)gp->scr_move(sp, LASTLINE(sp), 0);
	(void)gp->scr_clrtoeol(sp);
	for (; sp->t_rows > sp->t_minrows; --sp->t_rows, --TMAP) {
		(void)gp->scr_move(sp, TMAP - HMAP, 0);
		(void)gp->scr_clrtoeol(sp);
	}
	return (0);
}

/*
 * vs_sm_1down --
 *	Scroll the SMAP down one.
 *
 * PUBLIC: int vs_sm_1down __P((SCR *));
 */
int
vs_sm_1down(sp)
	SCR *sp;
{
	/*
	 * Insert a line at the top of the screen.  Shift the screen map
	 * down and display a new line at the top of the screen.
	 */
	(void)sp->gp->scr_move(sp, 0, 0);
	if (vs_insertln(sp, 1))
		return (1);

	/* One-line screens can fail. */
	if (IS_ONELINE(sp)) {
		if (vs_sm_prev(sp, HMAP, HMAP))
			return (1);
	} else {
		memmove(HMAP + 1, HMAP, (sp->rows - 1) * sizeof(SMAP));
		if (vs_sm_prev(sp, HMAP + 1, HMAP))
			return (1);
	}
	/* vs_sm_prev() flushed the cache. */
	return (vs_line(sp, HMAP, NULL, NULL));
}

/*
 * vs_insertln --
 *	Insert a line a la curses, make sure to put the information
 *	line and other screens back.
 */
static int
vs_insertln(sp, cnt)
	SCR *sp;
	int cnt;
{
	GS *gp;
	size_t oldy, oldx;

	gp = sp->gp;
	if (IS_ONELINE(sp)) {
		(void)gp->scr_move(sp, LASTLINE(sp), 0);
		(void)gp->scr_clrtoeol(sp);
	} else {
		(void)gp->scr_cursor(sp, &oldy, &oldx);
		while (cnt--) {
			(void)gp->scr_move(sp, LASTLINE(sp) - 1, 0);
			(void)gp->scr_deleteln(sp);
			(void)gp->scr_move(sp, oldy, oldx);
			(void)gp->scr_insertln(sp);
		}
	}
	return (0);
}

/*
 * vs_sm_next --
 *	Fill in the next entry in the SMAP.
 *
 * PUBLIC: int vs_sm_next __P((SCR *, SMAP *, SMAP *));
 */
int
vs_sm_next(sp, p, t)
	SCR *sp;
	SMAP *p, *t;
{
	size_t lcnt;

	SMAP_FLUSH(t);
	if (O_ISSET(sp, O_LEFTRIGHT)) {
		t->lno = p->lno + 1;
		t->coff = p->coff;
	} else {
		lcnt = vs_screens(sp, p->lno, NULL);
		if (lcnt == p->soff) {
			t->lno = p->lno + 1;
			t->soff = 1;
		} else {
			t->lno = p->lno;
			t->soff = p->soff + 1;
		}
	}
	return (0);
}

/*
 * vs_sm_prev --
 *	Fill in the previous entry in the SMAP.
 *
 * PUBLIC: int vs_sm_prev __P((SCR *, SMAP *, SMAP *));
 */
int
vs_sm_prev(sp, p, t)
	SCR *sp;
	SMAP *p, *t;
{
	SMAP_FLUSH(t);
	if (O_ISSET(sp, O_LEFTRIGHT)) {
		t->lno = p->lno - 1;
		t->coff = p->coff;
	} else {
		if (p->soff != 1) {
			t->lno = p->lno;
			t->soff = p->soff - 1;
		} else {
			t->lno = p->lno - 1;
			t->soff = vs_screens(sp, t->lno, NULL);
		}
	}
	return (t->lno == 0);
}

/*
 * vs_sm_cursor --
 *	Return the SMAP entry referenced by the cursor.
 *
 * PUBLIC: int vs_sm_cursor __P((SCR *, SMAP **));
 */
int
vs_sm_cursor(sp, smpp)
	SCR *sp;
	SMAP **smpp;
{
	SMAP *p;

	/* See if the cursor is not in the map. */
	if (sp->lno < HMAP->lno || sp->lno > TMAP->lno)
		return (1);

	/* Find the first occurence of the line. */
	for (p = HMAP; p->lno != sp->lno; ++p);

	/* Fill in the map information until we find the right line. */
	for (; p <= TMAP; ++p) {
		/* Short lines are common and easy to detect. */
		if (p != TMAP && (p + 1)->lno != p->lno) {
			*smpp = p;
			return (0);
		}
		if (!SMAP_CACHE(p) && vs_line(sp, p, NULL, NULL))
			return (1);
		if (p->c_eboff >= sp->cno) {
			*smpp = p;
			return (0);
		}
	}

	/* It was past the end of the map after all. */
	return (1);
}

/*
 * vs_sm_position --
 *	Return the line/column of the top, middle or last line on the screen.
 *	(The vi H, M and L commands.)  Here because only the screen routines
 *	know what's really out there.
 *
 * PUBLIC: int vs_sm_position __P((SCR *, MARK *, u_long, pos_t));
 */
int
vs_sm_position(sp, rp, cnt, pos)
	SCR *sp;
	MARK *rp;
	u_long cnt;
	pos_t pos;
{
	SMAP *smp;
	recno_t last;

	switch (pos) {
	case P_TOP:
		/*
		 * !!!
		 * Historically, an invalid count to the H command failed.
		 * We do nothing special here, just making sure that H in
		 * an empty screen works.
		 */
		if (cnt > TMAP - HMAP)
			goto sof;
		smp = HMAP + cnt;
		if (cnt && !db_exist(sp, smp->lno)) {
sof:			msgq(sp, M_BERR, "220|Movement past the end-of-screen");
			return (1);
		}
		break;
	case P_MIDDLE:
		/*
		 * !!!
		 * Historically, a count to the M command was ignored.
		 * If the screen isn't filled, find the middle of what's
		 * real and move there.
		 */
		if (!db_exist(sp, TMAP->lno)) {
			if (db_last(sp, &last))
				return (1);
			for (smp = TMAP; smp->lno > last && smp > HMAP; --smp);
			if (smp > HMAP)
				smp -= (smp - HMAP) / 2;
		} else
			smp = (HMAP + (TMAP - HMAP) / 2) + cnt;
		break;
	case P_BOTTOM:
		/*
		 * !!!
		 * Historically, an invalid count to the L command failed.
		 * If the screen isn't filled, find the bottom of what's
		 * real and try to offset from there.
		 */
		if (cnt > TMAP - HMAP)
			goto eof;
		smp = TMAP - cnt;
		if (!db_exist(sp, smp->lno)) {
			if (db_last(sp, &last))
				return (1);
			for (; smp->lno > last && smp > HMAP; --smp);
			if (cnt > smp - HMAP) {
eof:				msgq(sp, M_BERR,
			    "221|Movement past the beginning-of-screen");
				return (1);
			}
			smp -= cnt;
		}
		break;
	default:
		abort();
	}

	/* Make sure that the cached information is valid. */
	if (!SMAP_CACHE(smp) && vs_line(sp, smp, NULL, NULL))
		return (1);
	rp->lno = smp->lno;
	rp->cno = smp->c_sboff;

	return (0);
}

/*
 * vs_sm_nlines --
 *	Return the number of screen lines from an SMAP entry to the
 *	start of some file line, less than a maximum value.
 *
 * PUBLIC: recno_t vs_sm_nlines __P((SCR *, SMAP *, recno_t, size_t));
 */
recno_t
vs_sm_nlines(sp, from_sp, to_lno, max)
	SCR *sp;
	SMAP *from_sp;
	recno_t to_lno;
	size_t max;
{
	recno_t lno, lcnt;

	if (O_ISSET(sp, O_LEFTRIGHT))
		return (from_sp->lno > to_lno ?
		    from_sp->lno - to_lno : to_lno - from_sp->lno);

	if (from_sp->lno == to_lno)
		return (from_sp->soff - 1);

	if (from_sp->lno > to_lno) {
		lcnt = from_sp->soff - 1;	/* Correct for off-by-one. */
		for (lno = from_sp->lno; --lno >= to_lno && lcnt <= max;)
			lcnt += vs_screens(sp, lno, NULL);
	} else {
		lno = from_sp->lno;
		lcnt = (vs_screens(sp, lno, NULL) - from_sp->soff) + 1;
		for (; ++lno < to_lno && lcnt <= max;)
			lcnt += vs_screens(sp, lno, NULL);
	}
	return (lcnt);
}
