/* $OpenBSD: cmode.c,v 1.23 2024/05/21 05:00:48 jsg Exp $ */
/*
 * This file is in the public domain.
 *
 * Author: Kjell Wooding <kjell@openbsd.org>
 */

/*
 * Implement an non-irritating KNF-compliant mode for editing
 * C code.
 */

#include <sys/queue.h>
#include <ctype.h>
#include <signal.h>
#include <stdio.h>

#include "def.h"
#include "funmap.h"
#include "kbd.h"

/* Pull in from modes.c */
extern int changemode(int, int, char *);

static int cc_strip_trailp = TRUE;	/* Delete Trailing space? */
static int cc_basic_indent = 8;		/* Basic Indent multiple */
static int cc_cont_indent = 4;		/* Continued line indent */
static int cc_colon_indent = -8;	/* Label / case indent */

static int getmatch(int, int);
static int getindent(const struct line *, int *);
static int in_whitespace(struct line *, int);
static int findcolpos(const struct buffer *, const struct line *, int);
static struct line *findnonblank(struct line *);
static int isnonblank(const struct line *, int);

void cmode_init(void);

/* Keymaps */

static PF cmode_brace[] = {
	cc_brace,	/* } */
};

static PF cmode_cCP[] = {
	compile,		/* C-c P */
};


static PF cmode_cc[] = {
	NULL,		/* ^C */
	rescan,		/* ^D */
	rescan,		/* ^E */
	rescan,		/* ^F */
	rescan,		/* ^G */
	rescan,		/* ^H */
	cc_tab,		/* ^I */
	rescan,		/* ^J */
	rescan,		/* ^K */
	rescan,		/* ^L */
	cc_lfindent,	/* ^M */
};

static PF cmode_spec[] = {
	cc_char,	/* : */
};

static struct KEYMAPE (1) cmode_cmap = {
	1,
	1,
	rescan,
	{
		{ 'P', 'P', cmode_cCP, NULL }
	}
};

static struct KEYMAPE (3) cmodemap = {
	3,
	3,
	rescan,
	{
		{ CCHR('C'), CCHR('M'), cmode_cc, (KEYMAP *) &cmode_cmap },
		{ ':', ':', cmode_spec, NULL },
		{ '}', '}', cmode_brace, NULL }
	}
};

/* Function, Mode hooks */

void
cmode_init(void)
{
	funmap_add(cmode, "c-mode", 0);
	funmap_add(cc_char, "c-handle-special-char", 0);
	funmap_add(cc_brace, "c-handle-special-brace", 0);
	funmap_add(cc_tab, "c-tab-or-indent", 0);
	funmap_add(cc_indent, "c-indent", 0);
	funmap_add(cc_lfindent, "c-indent-and-newline", 0);
	maps_add((KEYMAP *)&cmodemap, "c");
}

/*
 * Enable/toggle c-mode
 */
int
cmode(int f, int n)
{
	return(changemode(f, n, "c"));
}

/*
 * Handle special C character - selfinsert then indent.
 */
int
cc_char(int f, int n)
{
	if (n < 0)
		return (FALSE);
	if (selfinsert(FFRAND, n) == FALSE)
		return (FALSE);
	return (cc_indent(FFRAND, n));
}

/*
 * Handle special C character - selfinsert then indent.
 */
int
cc_brace(int f, int n)
{
	if (n < 0)
		return (FALSE);
	if (showmatch(FFRAND, 1) == FALSE)
		return (FALSE);
	return (cc_indent(FFRAND, n));
}


/*
 * If we are in the whitespace at the beginning of the line,
 * simply act as a regular tab. If we are not, indent
 * current line according to whitespace rules.
 */
int
cc_tab(int f, int n)
{
	int inwhitep = FALSE;	/* In leading whitespace? */

	inwhitep = in_whitespace(curwp->w_dotp, llength(curwp->w_dotp));

	/* If empty line, or in whitespace */
	if (llength(curwp->w_dotp) == 0 || inwhitep)
		return (selfinsert(f, n));

	return (cc_indent(FFRAND, 1));
}

/*
 * Attempt to indent current line according to KNF rules.
 */
int
cc_indent(int f, int n)
{
	int pi, mi;			/* Previous indents (mi is ignored) */
	int ci;				/* current indent */
	struct line *lp;
	int ret;

	if (n < 0)
		return (FALSE);

	undo_boundary_enable(FFRAND, 0);
	if (cc_strip_trailp)
		deltrailwhite(FFRAND, 1);

	/*
	 * Search backwards for a non-blank, non-preprocessor,
	 * non-comment line
	 */

	lp = findnonblank(curwp->w_dotp);

	pi = getindent(lp, &mi);

	/* Strip leading space on current line */
	delleadwhite(FFRAND, 1);
	/* current indent is computed only to current position */
	(void)getindent(curwp->w_dotp, &ci);

	if (pi + ci < 0)
		ret = indent(FFOTHARG, 0);
	else
		ret = indent(FFOTHARG, pi + ci);

	undo_boundary_enable(FFRAND, 1);

	return (ret);
}

/*
 * Indent-and-newline (technically, newline then indent)
 */
int
cc_lfindent(int f, int n)
{
	if (n < 0)
		return (FALSE);
	if (cc_strip_trailp)
		(void)delwhite(FFRAND, 1);
	if (enewline(FFRAND, 1) == FALSE)
		return (FALSE);
	return (cc_indent(FFRAND, n));
}

/*
 * Get the level of indentation after line lp is processed
 * Note getindent has two returns:
 * curi = value if indenting current line.
 * return value = value affecting subsequent lines.
 */
static int
getindent(const struct line *lp, int *curi)
{
	int lo, co;		/* leading space,  current offset*/
	int nicol = 0;		/* position count */
	int c = '\0';		/* current char */
	int newind = 0;		/* new index value */
	int stringp = FALSE;	/* in string? */
	int escp = FALSE;	/* Escape char? */
	int lastc = '\0';	/* Last matched string delimiter */
	int nparen = 0;		/* paren count */
	int obrace = 0;		/* open brace count */
	int cbrace = 0;		/* close brace count */
	int firstnwsp = FALSE;	/* First nonspace encountered? */
	int colonp = FALSE;	/* Did we see a colon? */
	int questionp = FALSE;	/* Did we see a question mark? */
	int slashp = FALSE;	/* Slash? */
	int astp = FALSE;	/* Asterisk? */
	int cpos = -1;		/* comment position */
	int cppp  = FALSE;	/* Preprocessor command? */

	*curi = 0;

	/* Compute leading space */
	for (lo = 0; lo < llength(lp); lo++) {
		if (!isspace(c = lgetc(lp, lo)))
			break;
		if (c == '\t')
			nicol = ntabstop(nicol, curbp->b_tabw);
		else
			nicol++;
	}

	/* If last line was blank, choose 0 */
	if (lo == llength(lp))
		nicol = 0;

	newind = 0;
	/* Compute modifiers */
	for (co = lo; co < llength(lp); co++) {
		c = lgetc(lp, co);
		/* We have a non-whitespace char */
		if (!firstnwsp && !isspace(c)) {
			if (c == '#')
				cppp = TRUE;
			firstnwsp = TRUE; 
		}
		if (c == '\\')
			escp = !escp;
		else if (stringp) {
			if (!escp && (c == '"' || c == '\'')) {
				/* unescaped string char */
				if (getmatch(c, lastc))
					stringp = FALSE;
			}
		} else if (c == '"' || c == '\'') {
			stringp = TRUE;
			lastc = c;
		} else if (c == '(') {
			nparen++;
		} else if (c == ')') {
			nparen--;
		} else if (c == '{') {
			obrace++;
			firstnwsp = FALSE;
		} else if (c == '}') {
			cbrace++;
		} else if (c == '?') {
			questionp = TRUE;
		} else if (c == ':') {
			/* ignore (foo ? bar : baz) construct */
			if (!questionp)
				colonp = TRUE;
		} else if (c == '/') {
			/* first nonwhitespace? -> indent */
			if (firstnwsp) {
				/* If previous char asterisk -> close */
				if (astp)
					cpos = -1;
				else
					slashp = TRUE;
			}
		} else if (c == '*') {
			/* If previous char slash -> open */
			if (slashp)
				cpos = co;
			else
				astp = TRUE;
		} else if (firstnwsp) {
			firstnwsp = FALSE;
		}

		/* Reset matches that apply to next character only */
		if (c != '\\')
			escp = FALSE;
		if (c != '*')
			astp = FALSE;
		if (c != '/')
			slashp = FALSE;
	}
	/*
	 * If not terminated with a semicolon, and brace or paren open.
	 * we continue
	 */
	if (colonp) {
		*curi += cc_colon_indent;
		newind -= cc_colon_indent;
	}

	*curi -= (cbrace) * cc_basic_indent;
	newind += obrace * cc_basic_indent;

	if (nparen < 0)
		newind -= cc_cont_indent;
	else if (nparen > 0)
		newind += cc_cont_indent;

	*curi += nicol;

	/* Ignore preprocessor. Otherwise, add current column */
	if (cppp) {
		newind = nicol;
		*curi = 0;
	} else {
		newind += nicol;
	}

	if (cpos != -1)
		newind = findcolpos(curbp, lp, cpos);

	return (newind);
}

/*
 * Given a delimiter and its purported mate, tell us if they
 * match.
 */
static int
getmatch(int c, int mc)
{
	int match = FALSE;

	switch (c) {
	case '"':
		match = (mc == '"');
		break;
	case '\'':
		match = (mc == '\'');
		break;
	case '(':
		match = (mc == ')');
		break;
	case '[':
		match = (mc == ']');
		break;
	case '{':
		match = (mc == '}');
		break;
	}

	return (match);
}

static int
in_whitespace(struct line *lp, int len)
{
	int lo;
	int inwhitep = FALSE;

	for (lo = 0; lo < len; lo++) {
		if (!isspace(lgetc(lp, lo)))
			break;
		if (lo == len - 1)
			inwhitep = TRUE;
	}

	return (inwhitep);
}


/* convert a line/offset pair to a column position (for indenting) */
static int
findcolpos(const struct buffer *bp, const struct line *lp, int lo)
{
	int	col, i, c;
	char tmp[5];

	/* determine column */
	col = 0;

	for (i = 0; i < lo; ++i) {
		c = lgetc(lp, i);
		if (c == '\t') {
			col = ntabstop(col, curbp->b_tabw);
		} else if (ISCTRL(c) != FALSE)
			col += 2;
		else if (isprint(c)) {
			col++;
		} else {
			col += snprintf(tmp, sizeof(tmp), "\\%o", c);
		}

	}
	return (col);
}

/*
 * Find a non-blank line, searching backwards from the supplied line pointer.
 * For C, nonblank is non-preprocessor, non C++, and accounts
 * for complete C-style comments.
 */
static struct line *
findnonblank(struct line *lp)
{
	int lo;
	int nonblankp = FALSE;
	int commentp = FALSE;
	int slashp;
	int astp;
	int c;

	while (lback(lp) != curbp->b_headp && (commentp || !nonblankp)) {
		lp = lback(lp);
		slashp = FALSE;
		astp = FALSE;

		/* Potential nonblank? */
		nonblankp = isnonblank(lp, llength(lp));

		/*
		 * Search from end, removing complete C-style
		 * comments. If one is found, ignore it and
		 * test for nonblankness from where it starts.
		 */
		for (lo = llength(lp) - 1; lo >= 0; lo--) {
			if (!isspace(c = lgetc(lp, lo))) {
				if (commentp) { /* find comment "open" */
					if (c == '*')
						astp = TRUE;
					else if (astp && c == '/') {
						commentp = FALSE;
						/* whitespace to here? */
						nonblankp = isnonblank(lp, lo);
					}
				} else { /* find comment "close" */
					if (c == '/')
						slashp = TRUE;
					else if (slashp && c == '*')
						/* found a comment */
						commentp = TRUE;
				}
			}
		}
	}

	/* Rewound to start of file? */
	if (lback(lp) == curbp->b_headp && !nonblankp)
		return (curbp->b_headp);

	return (lp);
}

/*
 * Given a line, scan forward to 'omax' and determine if we
 * are all C whitespace.
 * Note that preprocessor directives and C++-style comments
 * count as whitespace. C-style comments do not, and must
 * be handled elsewhere.
 */
static int
isnonblank(const struct line *lp, int omax)
{
	int nonblankp = FALSE;		/* Return value */
	int slashp = FALSE;		/* Encountered slash */
	int lo;				/* Loop index */
	int c;				/* char being read */

	/* Scan from front for preprocessor, C++ comments */
	for (lo = 0; lo < omax; lo++) {
		if (!isspace(c = lgetc(lp, lo))) {
			/* Possible nonblank line */
			nonblankp = TRUE;
			/* skip // and # starts */
			if (c == '#' || (slashp && c == '/')) {
				nonblankp = FALSE;
				break;
			} else if (!slashp && c == '/') {
				slashp = TRUE;
				continue;
			}
		}
		slashp = FALSE;
	}
	return (nonblankp);
}
