/* -*- mode: C; mode: fold -*- */
/*
 This file is part of MOST.

 Copyright (c) 1991, 1999, 2002, 2005-2018, 2019 John E. Davis

 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., 675
 Mass Ave, Cambridge, MA 02139, USA.
*/
#include "config.h"

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <slang.h>

#include "most.h"
#include "window.h"
#include "line.h"
#include "file.h"
#include "display.h"
#include "search.h"

/* Note!!!  The regular expression searches may not work.  I have not
 * tested them.
 * FIXME!!!  This whole thing needs to be rewritten.  It is a mess.
 */

int Most_Case_Sensitive = 0;
char Most_Search_Str[256];
int Most_Search_Dir = 1;

#include "jdmacros.h"

typedef struct Search_Type_ Search_Type;
struct Search_Type_
{
   void *cd;
   unsigned char *(*fsearch_method) (Search_Type *, unsigned char *, unsigned char *, unsigned char *);
   unsigned char *(*bsearch_method) (Search_Type *, unsigned char *, unsigned char *, unsigned char *);
   void (*close_method)(Search_Type *);
};


static unsigned char Ascii_Upper[256];

static void bs_search_init (void)
{
   static int inited = 0;
   unsigned int i;

   if (inited) return;
   for (i = 0; i < 256; i++) Ascii_Upper[i] = i;
   for (i = 'a'; i <= 'z'; i++) Ascii_Upper[i] = (i - 32);
   inited = 1;
}

#define UPCASE(ch) (Ascii_Upper[(ch)])

#define CHAR_EQS(a,b) \
   (((a) == (b)) || (!Most_Case_Sensitive && (UPCASE(a) == UPCASE(b))))

static int is_ansi_escape (unsigned char **begp, unsigned char *end)
{
   unsigned char *p, ch;

   p = *begp;
   if ((p == end) || (*p++ != '[')) return 0;

   /* Look for "ESC X m", where X is one of:
    *   X = ""
    *   X = digit
    *   X = digit ; digit ...
    */
   ch = *p++;
   while (isdigit (ch))
     {
	while ((p < end) && isdigit (*p))
	  p++;

	if (p == end)
	  return 0;

	ch = *p++;
	if (ch == 'm')
	  break;

	if ((ch != ';') || (p == end))
	  return 0;

	ch = *p++;
     }
   if (ch != 'm')
     return 0;

   *begp = p;
   return 1;
}

static int is_rev_ansi_escape (unsigned char *beg, unsigned char **endp)
{
   unsigned char *p, ch;

   p = *endp;
   if (p == beg)
     return 0;

   ch = *p--;
   while (isdigit (ch))
     {
	while ((p > beg) && isdigit (*p))
	  p--;

	if (p == beg)
	  return 0;

	ch = *p--;

	if (ch == '[')
	  break;

	if ((ch != ';') || (p == beg))
	  return 0;

	ch = *p--;
     }

   if ((ch != '[') || (p < beg) || (*p != 033))
     return 0;

   *endp = p-1;
   return 1;
}

/* These routines have special processing for ANSI escape sequence and backspace handling.
 * For example, "hello world" may occur as:
 *   plain: hello world
 *   underlined: h_e_l_l_o_ world
 *   underlined: _h_e_l_l_o world
 *   bold: hheelllloo world
 *   ansi: [5mhello[m world
 *   ansi: [5mh[32;43mello[m world
 */

/* This routine returns the 1 + position of first match of key in str.
 * searches from beg up to but not including end.  Handles backspace, etc
 */
static unsigned char *
bs_fsearch (Search_Type *st,
	    unsigned char *beg, unsigned char *end,
	    unsigned char *key)
{
   unsigned char ch, ch1, ch1up;
   unsigned char *pos;
   int cis, key_len, j, str_len;

   (void) st;
   key_len = strlen ((char *)key);
   if (key_len == 0)
     return Most_Eob;

   str_len = (int) (end - beg);
   if (str_len < key_len) return (Most_Eob);

   cis = (Most_Case_Sensitive == 0);
   ch1 = key[0];
   ch1up = UPCASE(ch1);

   while (1)
     {
	/* Find first character that matches */
	while (1)
	  {
	     if (beg == end) return Most_Eob;

	     ch = *beg++;
	     if ((ch == ch1)
		 || (cis && (ch1up == UPCASE(ch))))
	       break;
	  }

	 /* so we have a position of possible match */
	j = 1;

	pos = beg;  /* save this position so we start from here again */

	while (1)
	  {
	     if (j == key_len)
	       return pos - 1;

	     if (beg == end)
	       break;

	     ch = *beg++;

	     /* FIXME:  This only works for x^Hx but not x^Hx^Hx...
	      * It is probably better to skip all the ^H characters
	      * until the end.  That is, regard "a^Hb^Hc" as 'c'.
	      */
	     if ((ch == 8)
		 && (beg + 1 < end)
		 && (CHAR_EQS(key[j - 1], *beg)
		     || (*beg == '_')))
	       {
		  ch = *(beg + 1);
		  beg += 2;
	       }
	     else if ((ch == '_') && (beg + 1 < end))
	       {
		  ch = *beg++;
		  if (ch == 8) ch = *beg++;
		  else
		    {
		       ch = '_';
		       beg--;
		    }
	       }
	     else if ((ch == 033) && is_ansi_escape (&beg, end))
	       continue;

	     if (!CHAR_EQS(ch, key[j]))
	       break;

	     j++;
	  }

	beg = pos;
     }
}

/*
 *  Search backwards in the buffer "beg" up to, but not including "end" for
 *  pattern "key".  It handles backspaces, etc
 */
static unsigned char *
bs_bsearch (Search_Type *st,
	    unsigned char *beg, unsigned char *end,
	    unsigned char *key)
{
   unsigned char ch, ch1, ch1up;
   unsigned char *pos;
   int key_len,j, str_len;
   int cis;

   (void) st;
   key_len = strlen ((char *)key);
   if (key_len == 0) return Most_Eob;

   str_len = (int) (end - beg);
   if (str_len < key_len) return Most_Eob;

   ch1 = key[key_len-1];
   ch1up = UPCASE(ch1);
   cis = (Most_Case_Sensitive == 0);

   while (1)
     {
	while (1)
	  {
	     if (end < beg)
	       return Most_Eob;

	     ch = *end--;
	     if ((ch == ch1)
		 || (cis && (ch1up == UPCASE(ch))))
	       break;
	  }

	pos = end;  /* save this position so we start from here again */

	j = key_len - 2;

	while (1)
	  {
	     if (j < 0)
	       return end + 1;
	     if (end < beg)
	       break;

	     ch = *end--;

	     if ((ch == 8)
		 && (end >= beg + 1)
		 && (CHAR_EQS(key[j + 1], *end)
		     || (*end == '_')))
	       {
		  ch = *(end - 1);
		  end -= 2;
	       }
	     else if ((ch == '_')
		      && (end >= beg + 1))
	       {
		  ch = *end--;
		  if (ch == 8) ch = *end--;
		  else
		    {
		       ch = '_';
		       end++;
		    }
	       }
	     else if ((ch == 'm') && is_rev_ansi_escape (beg, &end))
	       continue;

	     if (!CHAR_EQS(ch, key[j]))
	       break;

	     j--;
	  }
	end = pos;
     }
}

static int bs_open_search (Search_Type *st, char *key)
{
   (void) key;

   bs_search_init ();
   st->fsearch_method = bs_fsearch;
   st->bsearch_method = bs_bsearch;
   st->close_method = NULL;
   st->cd = NULL;
   return 0;
}

static unsigned char *sl_fsearch (Search_Type *st,
				  unsigned char *beg, unsigned char *end,
				  unsigned char *key)
{
   unsigned char *p;

   (void) key;
   if (NULL == (p = SLsearch_forward ((SLsearch_Type *)st->cd, beg, end)))
     p = Most_Eob;
   return p;
}

static unsigned char *sl_bsearch (Search_Type *st,
				  unsigned char *beg, unsigned char *end,
				  unsigned char *key)
{
   unsigned char *p;

   (void) key;
   if (NULL == (p = SLsearch_backward ((SLsearch_Type *)st->cd, beg, end, end)))
     p = Most_Eob;
   return p;
}

static void sl_search_close (Search_Type *st)
{
   if (st->cd != NULL)
     SLsearch_delete ((SLsearch_Type *) st->cd);
}

static int sl_open_search (Search_Type *st, char *key)
{
   unsigned int flags = 0;

   if (Most_Case_Sensitive == 0) flags |= SLSEARCH_CASELESS;
   if (Most_UTF8_Mode) flags |= SLSEARCH_UTF8;

   if (NULL == (st->cd = SLsearch_new ((SLuchar_Type *) key, flags)))
     return -1;

   st->fsearch_method = sl_fsearch;
   st->bsearch_method = sl_bsearch;
   st->close_method = sl_search_close;
   return 0;
}

static int
do_search_internal (Search_Type *st,
		    unsigned char *from, int repeat, MOST_INT *col)
{
   /* return the line match was found as well as line number,
    * search from i on; assume that line_array match the i so we need
    * no initial lookup */

   int test;
   MOST_INT save_line, the_col, row, s_len;
   char string[300];
   unsigned char *pos, *eob;
   unsigned int save_ofs;
   unsigned int found_ofs;

   if (*Most_Search_Str == 0)
     {
	most_message("Search string not specified.",1);
	return -1;
     }

   if ((from < Most_Beg) || (from > Most_Eob)) return -1;

   save_ofs = Most_C_Offset;
   save_line = Most_C_Line;
   found_ofs = Most_Eob - Most_Beg;
   *col = 0;
   s_len = strlen (Most_Search_Str);
   pos = from;

   eob = Most_Eob;

   test = repeat && (pos < Most_Eob) && (pos >= Most_Beg);
   while(test)
     {
	if (Most_Search_Dir == 1)
	  {
	     while (1)
	       {
		  unsigned int pos_ofs;

		  pos = (*st->fsearch_method)(st, pos, Most_Eob, (unsigned char*) Most_Search_Str);
		  pos_ofs = (unsigned int) (Most_Eob - Most_Beg);

		  if (pos < Most_Eob)
		    break;

		  if (0 == most_read_file_dsc (10, 0))
		    {
		       /* Pointer may be invalid after this call */
		       pos = Most_Beg + pos_ofs;
		       break;
		    }

		  /* This might need an adjustment */
		  pos = Most_Beg + (pos_ofs - s_len);
		  if (pos < Most_Beg) pos = Most_Beg;
	       }
	  }
	else
	  pos = (*st->bsearch_method)(st, Most_Beg, pos, (unsigned char *) Most_Search_Str);

	if (pos < Most_Eob)
	  {
	     repeat--;
	     found_ofs = pos - Most_Beg;
	     if (Most_Search_Dir == 1)
	       pos += s_len;
	     else pos--;
	  }
	test = repeat && (pos < Most_Eob) && (pos >= Most_Beg);
	if (SLKeyBoard_Quit)
	  {
	     most_message ("Search Interrupted.", 1);
	     break;
	  }
     }

   if (eob != Most_Eob)
     Most_Num_Lines = most_count_lines (Most_Beg, Most_Eob);

   if (repeat) /* not found */
     {
	*col = 0;

	(void) sprintf(string,"Search failed: %s",Most_Search_Str);
	most_message(string,1);
	row = -1;
     }
   else /* if ( !Most_T_Opt && !Most_B_Opt) */   /* expand tabs to get col correct */
     {
	most_find_row_column(Most_Beg + found_ofs, &row, &the_col);
	if (Most_B_Opt) *col = the_col + 52;
	else
	  *col = 1 + most_apparant_distance(Most_Beg + found_ofs);
     }

   Most_C_Offset = save_ofs;
   Most_C_Line = save_line;
   if (row > 0) Most_Curs_Offset = found_ofs;

   return row;
}

static int search_internal (Search_Type *st, unsigned char *from, int repeat, MOST_INT *colp)
{
   int status;

   status = do_search_internal (st, from, repeat, colp);

   if (st->close_method != NULL)
     (st->close_method)(st);

   return status;
}

static int simple_search (unsigned char *from, int repeat, MOST_INT *colp)
{
   Search_Type st;

   if (Most_V_Opt || Most_B_Opt)
     {
	/* Nothing special about the ^H and _ chars.  User faster SLsearch */
	if (-1 == sl_open_search (&st, Most_Search_Str))
	  return -1;
     }
   else if (-1 == bs_open_search (&st, Most_Search_Str))
     return -1;

   return search_internal (&st, from, repeat, colp);
}


static void re_search_close (Search_Type *st)
{
   if (st->cd != NULL)
     SLregexp_free ((SLRegexp_Type *) st->cd);
}

static unsigned char *
re_fsearch (Search_Type *st,
	    unsigned char *beg, unsigned char *end,
	    unsigned char *key)
{
   SLRegexp_Type *re;
   unsigned char *p;
   unsigned int flags;

   (void) key;

   re = (SLRegexp_Type *)st->cd;
   (void) SLregexp_get_hints (re, &flags);

   while (beg < end)
     {
	unsigned char *line_end = beg;

	while (line_end < end)
	  {
	     unsigned char ch = *line_end++;
	     if (ch == '\n') break;
	  }

	p = (unsigned char *)SLregexp_match (re, (char *)beg, (line_end - beg));
	if (p != NULL)
	  {
	     if ((0 == (flags & SLREGEXP_HINT_BOL))
		 || (p != beg)
		 || (beg == Most_Beg)
		 || (*(beg - 1) == '\n'))
	       return p;
	  }
	beg = line_end;
     }

   return Most_Eob;
}

static unsigned char *
re_bsearch (Search_Type *st,
	    unsigned char *beg, unsigned char *end,
	    unsigned char *key)
{
   SLRegexp_Type *re;
   unsigned char *line_end, *eob;
   unsigned int flags;

   (void) key;
   re = (SLRegexp_Type *)st->cd;
   (void) SLregexp_get_hints (re, &flags);

   line_end = end;
   eob = Most_Eob;
   while (line_end < eob)
     {
	if (*line_end == '\n')
	  break;
	line_end++;
     }

   while (end > beg)
     {
	unsigned char *p, *match;
	unsigned char *line = end;
	while (line > beg)
	  {
	     line--;
	     if (*line == '\n')
	       {
		  line++;
		  break;
	       }
	  }

	/* line is now at the start of a line */
	if (NULL != (match = (unsigned char *)SLregexp_match (re, (char *)line, line_end-line)))
	  {
	     if (match >= end)
	       {
		  /* Match occurs to right of boundary.  Try previous line */
		  end = line_end = line-1;
		  continue;
	       }

	     if (flags & SLREGEXP_HINT_BOL)
	       return match;

	     /*    t    tt  z    t  t     t z */
	     /* Find match closest to end */
	     while ((line < end)
		    && (NULL != (p = (unsigned char *)SLregexp_match (re, (char *)line, (line_end - line))))
		    && (p < end))
	       {
		  match = p;
		  line++;
	       }
	     return match;
	  }

	end = line-1;
	line_end = end;
     }

   return Most_Eob;
}


static int regexp_search (unsigned char *from, int repeat, MOST_INT *colp)
{
   Search_Type st;
   SLRegexp_Type *re;
   char *pattern;
   unsigned int flags;

   pattern = Most_Search_Str;

   flags = 0;
   if (Most_Case_Sensitive == 0) flags |= SLREGEXP_CASELESS;

   re = SLregexp_compile (pattern, flags);
   if (re == NULL)
     return -1;

   (void) SLregexp_get_hints (re, &flags);
   if (flags & SLREGEXP_HINT_OSEARCH)
     {
	SLregexp_free (re);
	return simple_search (from, repeat, colp);
     }

   st.cd = (void *)re;
   st.fsearch_method = re_fsearch;
   st.bsearch_method = re_bsearch;
   st.close_method = re_search_close;

   return search_internal (&st, from, repeat, colp);
}

int most_search (unsigned char *from, int repeat, MOST_INT *colp)
{
   if (Most_Do_Regexp_Search)
     return regexp_search (from, repeat, colp);

   return simple_search (from, repeat, colp);
}
