/* vi:set ts=8 sts=4 sw=4:
 *
 * VIM - Vi IMproved	by Bram Moolenaar
 *
 * Do ":help uganda"  in Vim to read copying and usage conditions.
 * Do ":help credits" in Vim to see a list of people who contributed.
 */

/*
 * quickfix.c: functions for quickfix mode, using a file with error messages
 */

#include "vim.h"

#ifdef QUICKFIX

static void qf_free __ARGS((void));
static char_u *qf_types __ARGS((int, int));

/*
 * for each error the next struct is allocated and linked in a list
 */
struct qf_line
{
    struct qf_line  *qf_next;	/* pointer to next error in the list */
    struct qf_line  *qf_prev;	/* pointer to previous error in the list */
    linenr_t	     qf_lnum;	/* line number where the error occurred */
    int		     qf_fnum;	/* file number for the line */
    int		     qf_col;	/* column where the error occurred */
    int		     qf_nr;	/* error number */
    char_u	    *qf_text;	/* description of the error */
    char_u	     qf_cleared;/* set to TRUE if line has been deleted */
    char_u	     qf_type;	/* type of the error (mostly 'E') */
    char_u	     qf_valid;	/* valid error message detected */
};

static struct qf_line *qf_start;	/* pointer to the first error */
static struct qf_line *qf_ptr;		/* pointer to the current error */

static int  qf_count = 0;	/* number of errors (0 means no error list) */
static int  qf_index;		/* current index in the error list */
static int  qf_nonevalid;	/* set to TRUE if not a single valid entry found */

#define MAX_ADDR    7		/* maximum number of % recognized, also adjust
				    sscanf() below */

/*
 * Structure used to hold the info of one part of 'errorformat'
 */
struct eformat
{
    char_u	    *fmtstr;	    /* pre-formatted part of 'errorformat' */
#ifdef UTS2
    char_u	    *(adr[MAX_ADDR]);	/* addresses used */
#else
    void	    *(adr[MAX_ADDR]);
#endif
    int		    adr_cnt;	    /* number of addresses used */
    struct eformat  *next;	    /* pointer to next (NULL if last) */
};

/*
 * Read the errorfile into memory, line by line, building the error list.
 * Return -1 for error, number of errors for success.
 */
    int
qf_init(efile)
    char_u	    *efile;
{
    char_u	    *namebuf;
    char_u	    *errmsg;
    int		    col;
    int		    type;
    int		    valid;
    long	    lnum;
    int		    enr;
    FILE	    *fd;
    struct qf_line  *qfp = NULL;
    struct qf_line  *qfprev = NULL;	/* init to make SASC shut up */
    char_u	    *efmp;
    struct eformat  *fmt_first = NULL;
    struct eformat  *fmt_last = NULL;
    struct eformat  *fmt_ptr;
    char_u	    *efm;
    int		    maxlen;
    int		    len;
    int		    i, j;
    int		    retval = -1;	/* default: return error flag */

    if (efile == NULL)
	return FAIL;

    namebuf = alloc(CMDBUFFSIZE + 1);
    errmsg = alloc(CMDBUFFSIZE + 1);
    if (namebuf == NULL || errmsg == NULL)
	goto qf_init_end;

    if ((fd = fopen((char *)efile, "r")) == NULL)
    {
	emsg2(e_openerrf, efile);
	goto qf_init_end;
    }
    qf_free();
    qf_index = 0;

/*
 * Each part of the format string is copied and modified from p_efm to fmtstr.
 * Only a few % characters are allowed.
 */
    efm = p_efm;
    while (efm[0])
    {
	/*
	 * Allocate a new eformat structure and put it at the end of the list
	 */
	fmt_ptr = (struct eformat *)alloc((unsigned)sizeof(struct eformat));
	if (fmt_ptr == NULL)
	    goto error2;
	if (fmt_first == NULL)	    /* first one */
	    fmt_first = fmt_ptr;
	else
	    fmt_last->next = fmt_ptr;
	fmt_last = fmt_ptr;
	fmt_ptr->next = NULL;
	fmt_ptr->adr_cnt = 0;

	/*
	 * Isolate one part in the 'errorformat' option
	 */
	for (len = 0; efm[len] != NUL && efm[len] != ','; ++len)
	    if (efm[len] == '\\' && efm[len + 1] != NUL)
		++len;

	/*
	 * Get some space to modify the format string into.
	 * Must be able to do the largest expansion (x3) MAX_ADDR times.
	 */
	maxlen = len + MAX_ADDR * 3 + 4;
	if ((fmt_ptr->fmtstr = alloc(maxlen)) == NULL)
	    goto error2;

	for (i = 0; i < MAX_ADDR; ++i)
	    fmt_ptr->adr[i] = NULL;

	for (efmp = efm, i = 0; efmp < efm + len; ++efmp, ++i)
	{
	    if (efmp[0] != '%')		    /* copy normal character */
	    {
		if (efmp[0] == '\\' && efmp + 1 < efm + len)
		    ++efmp;
		fmt_ptr->fmtstr[i] = efmp[0];
	    }
	    else
	    {
		fmt_ptr->fmtstr[i++] = '%';
		switch (efmp[1])
		{
		case 'f':	/* file name */
			fmt_ptr->adr[fmt_ptr->adr_cnt++] = namebuf;
			/* FALLTHROUGH */

		case 'm':	/* message */
			if (efmp[1] == 'm')
			    fmt_ptr->adr[fmt_ptr->adr_cnt++] = errmsg;
			fmt_ptr->fmtstr[i++] = '[';
			fmt_ptr->fmtstr[i++] = '^';
#ifdef __EMX__
			/* don't allow spaces in file name. This fixes
			 * the broken sscanf() where an empty message
			 * is accepted as a valid conversion.
			 */
			if (efmp[1] == 'f')
			    fmt_ptr->fmtstr[i++] = ' ';
#endif
			if (efmp[2] == '\\')	    /* could be "%m\," */
			    j = 3;
			else
			    j = 2;
			if (efmp + j < efm + len)
			    fmt_ptr->fmtstr[i++] = efmp[j];
			else
			{
			    /*
			     * The %f or %m is the last one in the format,
			     * stop at the CR of NL at the end of the line.
			     */
#ifdef USE_CRNL
			    fmt_ptr->fmtstr[i++] = '\r';
#endif
			    fmt_ptr->fmtstr[i++] = '\n';
			}
			fmt_ptr->fmtstr[i] = ']';
			break;
		case 'c':	/* column */
			fmt_ptr->adr[fmt_ptr->adr_cnt++] = &col;
			fmt_ptr->fmtstr[i] = 'd';
			break;
		case 'l':	/* line */
			fmt_ptr->adr[fmt_ptr->adr_cnt++] = &lnum;
			fmt_ptr->fmtstr[i++] = 'l';
			fmt_ptr->fmtstr[i] = 'd';
			break;
		case 'n':	/* error number */
			fmt_ptr->adr[fmt_ptr->adr_cnt++] = &enr;
			fmt_ptr->fmtstr[i] = 'd';
			break;
		case 't':	/* error type */
			fmt_ptr->adr[fmt_ptr->adr_cnt++] = &type;
			fmt_ptr->fmtstr[i] = 'c';
			break;
		case '%':	/* %% */
		case '*':	/* %*: no assignment */
			fmt_ptr->fmtstr[i] = efmp[1];
			break;
		default:
			EMSG("invalid % in format string");
			goto error2;
		}
		if (fmt_ptr->adr_cnt == MAX_ADDR)
		{
		    EMSG("too many % in format string");
		    goto error2;
		}
		++efmp;
	    }
	    if (i >= maxlen - 6)
	    {
		EMSG("invalid format string");
		goto error2;
	    }
	}
	fmt_ptr->fmtstr[i] = NUL;

	/*
	 * Advance to next part
	 */
	efm = skip_to_option_part(efm + len);	/* skip comma and spaces */
    }
    if (fmt_first == NULL)	/* nothing found */
    {
	EMSG("'errorformat' contains no pattern");
	goto error2;
    }

    /*
     * got_int is reset here, because it was probably set when killing the
     * ":make" command, but we still want to read the errorfile then.
     */
    got_int = FALSE;

    /*
     * Read the lines in the error file one by one.
     * Try to recognize one of the error formats in each line.
     */
    while (fgets((char *)IObuff, CMDBUFFSIZE, fd) != NULL && !got_int)
    {
	if ((qfp = (struct qf_line *)alloc((unsigned)sizeof(struct qf_line)))
								      == NULL)
	    goto error2;

	IObuff[CMDBUFFSIZE] = NUL;  /* for very long lines */

	/*
	 * Try to match each part of 'errorformat' until we find a complete
	 * match or none matches.
	 */
	valid = TRUE;
	for (fmt_ptr = fmt_first; fmt_ptr != NULL; fmt_ptr = fmt_ptr->next)
	{
	    namebuf[0] = NUL;
	    errmsg[0] = NUL;
	    lnum = 0;
	    col = 0;
	    enr = -1;
	    type = 0;

	    /*
	     * If first char of the format and message don't match, there is
	     * no need to try sscanf() on it... Somehow I believe there are
	     * very slow implementations of sscanf().
	     * -- Paul Slootman
	     */
	    if (fmt_ptr->fmtstr[0] != '%' && fmt_ptr->fmtstr[0] != IObuff[0])
		continue;

	    if (sscanf((char *)IObuff, (char *)fmt_ptr->fmtstr,
			fmt_ptr->adr[0], fmt_ptr->adr[1], fmt_ptr->adr[2],
			fmt_ptr->adr[3], fmt_ptr->adr[4], fmt_ptr->adr[5],
			fmt_ptr->adr[6]) == fmt_ptr->adr_cnt)
		break;
	}
	if (fmt_ptr == NULL)
	{
	    namebuf[0] = NUL;		/* no match found, remove file name */
	    lnum = 0;			/* don't jump to this line */
	    valid = FALSE;
	    STRCPY(errmsg, IObuff);	/* copy whole line to error message */
	    if ((efmp = vim_strrchr(errmsg, '\n')) != NULL)
		*efmp = NUL;
#ifdef USE_CRNL
	    if ((efmp = vim_strrchr(errmsg, '\r')) != NULL)
		*efmp = NUL;
#endif
	}

	if (namebuf[0] == NUL)		/* no file name */
	    qfp->qf_fnum = 0;
	else
	    qfp->qf_fnum = buflist_add(namebuf);
	if ((qfp->qf_text = vim_strsave(errmsg)) == NULL)
	    goto error1;
	if (!vim_isprintc(type))	/* only printable chars allowed */
	    type = 0;
	qfp->qf_lnum = lnum;
	qfp->qf_col = col;
	qfp->qf_nr = enr;
	qfp->qf_type = type;
	qfp->qf_valid = valid;

	if (qf_count == 0)	/* first element in the list */
	{
	    qf_start = qfp;
	    qfp->qf_prev = qfp;	/* first element points to itself */
	}
	else
	{
	    qfp->qf_prev = qfprev;
	    qfprev->qf_next = qfp;
	}
	qfp->qf_next = qfp;	/* last element points to itself */
	qfp->qf_cleared = FALSE;
	qfprev = qfp;
	++qf_count;
	if (qf_index == 0 && qfp->qf_valid)	/* first valid entry */
	{
	    qf_index = qf_count;
	    qf_ptr = qfp;
	}
	line_breakcheck();
    }
    if (!ferror(fd))
    {
	if (qf_index == 0)	/* no valid entry found */
	{
	    qf_ptr = qf_start;
	    qf_index = 1;
	    qf_nonevalid = TRUE;
	}
	else
	    qf_nonevalid = FALSE;
	retval = qf_count;	/* return number of matches */
	goto qf_init_ok;
    }
    emsg(e_readerrf);
error1:
    vim_free(qfp);
error2:
    qf_free();
qf_init_ok:
    fclose(fd);
    for (fmt_ptr = fmt_first; fmt_ptr != NULL; fmt_ptr = fmt_first)
    {
	fmt_first = fmt_ptr->next;
	vim_free(fmt_ptr->fmtstr);
	vim_free(fmt_ptr);
    }
qf_init_end:
    vim_free(namebuf);
    vim_free(errmsg);
    return retval;
}

/*
 * jump to a quickfix line
 * if dir == FORWARD go "errornr" valid entries forward
 * if dir == BACKWARD go "errornr" valid entries backward
 * else if "errornr" is zero, redisplay the same line
 * else go to entry "errornr"
 */
    void
qf_jump(dir, errornr, forceit)
    int	    dir;
    int	    errornr;
    int	    forceit;
{
    struct qf_line  *old_qf_ptr;
    int		    old_qf_index;
    static char_u   *e_no_more_items = (char_u *)"No more items";
    char_u	    *err = e_no_more_items;
    linenr_t	    i;
    BUF		    *old_curbuf;

    if (qf_count == 0)
    {
	emsg(e_quickfix);
	return;
    }

    old_qf_ptr = qf_ptr;
    old_qf_index = qf_index;
    if (dir == FORWARD)	    /* next valid entry */
    {
	while (errornr--)
	{
	    old_qf_ptr = qf_ptr;
	    old_qf_index = qf_index;
	    do
	    {
		if (qf_index == qf_count || qf_ptr->qf_next == NULL)
		{
		    qf_ptr = old_qf_ptr;
		    qf_index = old_qf_index;
		    if (err != NULL)
		    {
			emsg(err);
			return;
		    }
		    errornr = 0;
		    break;
		}
		++qf_index;
		qf_ptr = qf_ptr->qf_next;
	    } while (!qf_nonevalid && !qf_ptr->qf_valid);
	    err = NULL;
	}
    }
    else if (dir == BACKWARD)	    /* previous valid entry */
    {
	while (errornr--)
	{
	    old_qf_ptr = qf_ptr;
	    old_qf_index = qf_index;
	    do
	    {
		if (qf_index == 1 || qf_ptr->qf_prev == NULL)
		{
		    qf_ptr = old_qf_ptr;
		    qf_index = old_qf_index;
		    if (err != NULL)
		    {
			emsg(err);
			return;
		    }
		    errornr = 0;
		    break;
		}
		--qf_index;
		qf_ptr = qf_ptr->qf_prev;
	    } while (!qf_nonevalid && !qf_ptr->qf_valid);
	    err = NULL;
	}
    }
    else if (errornr != 0)	/* go to specified number */
    {
	while (errornr < qf_index && qf_index > 1 && qf_ptr->qf_prev != NULL)
	{
	    --qf_index;
	    qf_ptr = qf_ptr->qf_prev;
	}
	while (errornr > qf_index && qf_index < qf_count && qf_ptr->qf_next != NULL)
	{
	    ++qf_index;
	    qf_ptr = qf_ptr->qf_next;
	}
    }

    /*
     * If there is a file name,
     * read the wanted file if needed, and check autowrite etc.
     */
    old_curbuf = curbuf;
    if (qf_ptr->qf_fnum == 0 || buflist_getfile(qf_ptr->qf_fnum,
				    (linenr_t)1, GETF_SETMARK, forceit) == OK)
    {
	/* When not switched to another buffer, still need to set pc mark */
	if (curbuf == old_curbuf)
	    setpcmark();

	/*
	 * Go to line with error, unless qf_lnum is 0.
	 */
	i = qf_ptr->qf_lnum;
	if (i > 0)
	{
	    if (i > curbuf->b_ml.ml_line_count)
		i = curbuf->b_ml.ml_line_count;
	    curwin->w_cursor.lnum = i;
	}
	if (qf_ptr->qf_col > 0)
	{
	    curwin->w_cursor.col = qf_ptr->qf_col - 1;
	    adjust_cursor();
	}
	else
	    beginline(BL_WHITE | BL_FIX);
	update_topline_redraw();
	smsg((char_u *)"(%d of %d)%s%s: %s", qf_index, qf_count,
	      qf_ptr->qf_cleared ? (char_u *)" (line deleted)" : (char_u *)"",
		    qf_types(qf_ptr->qf_type, qf_ptr->qf_nr), qf_ptr->qf_text);
	/*
	 * if the message is short, redisplay after redrawing the screen
	 */
	if (linetabsize(IObuff) < ((int)p_ch - 1) * Columns + sc_col)
	{
	    keep_msg = IObuff;
	    keep_msg_attr = 0;
	}
    }
    else if (qf_ptr->qf_fnum != 0)
    {
	/*
	 * Couldn't open file, so put index back where it was.	This could
	 * happen if the file was readonly and we changed something - webb
	 */
	qf_ptr = old_qf_ptr;
	qf_index = old_qf_index;
    }
}

/*
 * list all errors
 */
    void
qf_list(all)
    int all;	    /* If not :cl!, only show recognised errors */
{
    BUF		    *buf;
    char_u	    *fname;
    struct qf_line  *qfp;
    int		    i;

    if (qf_count == 0)
    {
	emsg(e_quickfix);
	return;
    }

    if (qf_nonevalid)
	all = TRUE;
    qfp = qf_start;
    for (i = 1; !got_int && i <= qf_count; ++i)
    {
	if (qfp->qf_valid || all)
	{
	    msg_putchar('\n');
	    fname = NULL;
	    if (qfp->qf_fnum != 0 &&
				 (buf = buflist_findnr(qfp->qf_fnum)) != NULL)
		fname = buf->b_fname;
	    if (fname == NULL)
		sprintf((char *)IObuff, "%2d", i);
	    else
		sprintf((char *)IObuff, "%2d %s", i, fname);
	    msg_outtrans_attr(IObuff, highlight_attr[HLF_D]);
	    if (qfp->qf_lnum == 0)
		IObuff[0] = NUL;
	    else if (qfp->qf_col == 0)
		sprintf((char *)IObuff, ":%ld", qfp->qf_lnum);
	    else
		sprintf((char *)IObuff, ":%ld, col %d",
						   qfp->qf_lnum, qfp->qf_col);
	    sprintf((char *)IObuff + STRLEN(IObuff), "%s: ",
					qf_types(qfp->qf_type, qfp->qf_nr));
	    msg_puts_attr(IObuff, highlight_attr[HLF_N]);
	    msg_prt_line(qfp->qf_text);
	    out_flush();		/* show one line at a time */
	}
	qfp = qfp->qf_next;
	ui_breakcheck();
    }
}

/*
 * free the error list
 */
    static void
qf_free()
{
    struct qf_line *qfp;

    while (qf_count)
    {
	qfp = qf_start->qf_next;
	vim_free(qf_start->qf_text);
	vim_free(qf_start);
	qf_start = qfp;
	--qf_count;
    }
}

/*
 * qf_mark_adjust: adjust marks
 */
   void
qf_mark_adjust(line1, line2, amount, amount_after)
    linenr_t	line1;
    linenr_t	line2;
    long	amount;
    long	amount_after;
{
    int		    i;
    struct qf_line  *qfp;

    if (qf_count)
	for (i = 0, qfp = qf_start; i < qf_count; ++i, qfp = qfp->qf_next)
	    if (qfp->qf_fnum == curbuf->b_fnum)
	    {
		if (qfp->qf_lnum >= line1 && qfp->qf_lnum <= line2)
		{
		    if (amount == MAXLNUM)
			qfp->qf_cleared = TRUE;
		    else
			qfp->qf_lnum += amount;
		}
		if (amount_after && qfp->qf_lnum > line2)
		    qfp->qf_lnum += amount_after;
	    }
}

/*
 * Make a nice message out of the error character and the error number:
 *  char    number	message
 *  e or E    0		"   error"
 *  w or W    0		" warning"
 *  0	      0		""
 *  other     0		" c"
 *  e or E    n		"   error n"
 *  w or W    n		" warning n"
 *  0	      n		"   error n"
 *  other     n		" c n"
 */
    static char_u *
qf_types(c, nr)
    int c, nr;
{
    static char_u	buf[20];
    static char_u	cc[3];
    char_u		*p;

    if (c == 'W' || c == 'w')
	p = (char_u *)" warning";
    else if (c == 'E' || c == 'e' || (c == 0 && nr > 0))
	p = (char_u *)"   error";
    else if (c == 0)
	p = (char_u *)"";
    else
    {
	cc[0] = ' ';
	cc[1] = c;
	cc[2] = NUL;
	p = cc;
    }

    if (nr <= 0)
	return p;

    sprintf((char *)buf, "%s %3d", p, nr);
    return buf;
}
#endif /* QUICKFIX */
