/* Perform queries on the contents of a GNATS database.
   Copyright (C) 1993 Free Software Foundation, Inc.
   Contributed by Brendan Kehoe (brendan@cygnus.com).

This file is part of GNU GNATS.

GNU GNATS 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, or (at your option)
any later version.

GNU GNATS 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 GNU GNATS; see the file COPYING.  If not, write to
the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.  */

#include <stdio.h>
#include <sys/types.h>
#include <getopt.h>
#include <time.h>
#include <ctype.h>

#include "config.h"
#include "gnats.h"
#include "pathmax.h"
#ifdef USE_RX
#include "rx.h"
#else
#include "regex.h"
#endif

extern char *xmalloc ();
extern char *basename ();

/* The name this program was run with.  */
char *program_name;

/* Where the results of query-pr should go.  */
FILE *outfile = stdout;

/* Where to keep the static index if necessary.  */
Index *index_chain;

/* If specified, search for the originator of this PR.  */
char *originator = NULL;

/* Whether or not to search the text fields, and the regexp to
   search for.  */
char *text_search = NULL;

/* Whether or not to search the multitext fields, and the regexp to
   search for.  */
char *m_text_search = NULL;

/* Whether or not we're searching more than one (or every) PR.  */
int searching = 0;

/* If 1, print the whole PR out, not just the choice bits.  */
int full_report = 0;

/* If 1, emit info for a SQL database.  */
int sql_format = 0;

/* If 1, don't talk about closed PRs.  */
int skip_closed = 0; /* FIXME: This doesn't make sense when we have
			configurable state names.  */

/* If 1, print a string like "/gnats/GNATS/g++/145:0:" for the
   emacs next-error function.  */
int print_path = 0;

/* If 1, emit a summary of the PR.  */
int summary = 0;

/* If 1, we found a match and should output a newline.  */
int found_match = 0;

/* If 1, don't allow redirection or viewing of confidential PRs.  */
int restricted = 0;

/* Defined in version.c.  */
extern char *version_string;

extern PR_entry pr[NUM_PR_ITEMS];

struct option long_options[] =
{
  {"category", 1, NULL, 'c'},
  {"confidential", 1, NULL, 'C'},
  {"directory", 1, NULL, 'd'},
  {"full", 0, NULL, 'F'},
  {"help", 0, NULL, 'h'},
  {"multitext", 1, NULL, 'm'},
  {"originator", 1, NULL, 'O'},
  {"output", 1, NULL, 'o'},
  {"priority", 1, NULL, 'p'},
  {"print-path", 0, NULL, 'P'},
  {"responsible", 1, NULL, 'r'},
  {"restricted", 0, NULL, 'R'},
  {"severity", 1, NULL, 'e'},
  {"skip-closed", 0, NULL, 'x'},
  {"sql", 0, NULL, 'i'},
  {"state", 1, NULL, 's'},
  {"summary", 0, NULL, 'q'},
  {"submitter", 1, NULL, 'S'},
  {"text", 1, NULL, 't'},
  {"version", 0, NULL, 'V'},
  {NULL, 0, NULL, 0}
};

void usage (), version ();

typedef enum { Severity, Priority, State, Class } Sql_Types;
static int
sql_types (p, type)
     char *p;
     Sql_Types type;
{
  switch (type)
    {
    case Severity:
      if (tolower(*p) == 'c')
	return 1;
      else if (tolower(*p) == 's')
	return 2;
      else if (tolower(*p) == 'n')
	return 3;
      break;
    case Priority:
      if (tolower(*p) == 'h')
	return 1;
      else if (tolower(*p) == 'm')
	return 2;
      else if (tolower(*p) == 'l')
	return 3;
      break;
    case State:
      if (tolower(*p) == 'o')
	return 1;
      else if (tolower(*p) == 'a')
	return 2;
      else if (tolower(*p) == 's')
	return 3;
      else if (tolower(*p) == 'f')
	return 4;
      else if (tolower(*p) == 'c')
	return 5;
      break;
    case Class:
      if (tolower(p[1]) == 'w') /* sw-bug */
	return 1;
      else if (tolower(p[1]) == 'o') /* doc-bug */
	return 2;
      else if (tolower(*p) == 's') /* support */
	return 3;
      else if (tolower(*p) == 'c') /* change-request */
	return 4;
      else if (tolower(*p) == 'm') /* mistaken */
	return 5;
      else if (tolower(*p) == 'd') /* duplicate */
	return 6;
      break;
    }

  return 0;
}

static char *
sql_time (s)
     char *s;
{
  extern time_t get_date ();
  time_t t;
  struct tm *ar_time;
  char *buf = (char *) xmalloc (16);

  t = get_date (s, NULL);
  ar_time = localtime (&t);
  strftime (buf, 16, "%y-%m-%d %H:%M", ar_time);

  return buf;
}

char *
get_category (p)
     char *p;
{
  FILE *fp;
  char *path, *category;

  fp = open_index ();
  if (fp == (FILE *)NULL)
    return NULL;

  category = find_pr_category (fp, p);

  if (category == NULL)
    return NULL;

  close_index (fp);

  path = (char *) xmalloc (PATH_MAX);
  if (category)
    sprintf (path, "%s/%s/%s", gnats_root, category, p);
  else
    sprintf (path, "%s/%s", gnats_root, p);

  return path;
}

int
get_pr (path, pr)
     char *path;
     char *pr;
{
  FILE *fp = fopen (path, "r");
  if (fp == (FILE *)NULL)
    {
      fprintf (stderr, "%s: couldn't read PR %s\n", program_name, pr);
      return 0;
    }

  /* Since read_header looks to see if the value's NULL or not, and we're
     getting a new PR, we need to make sure we've cleaned out the old
     stuff first.  */
  init_header ();
  read_header (fp);
  init_pr ();
  read_pr (fp, !full_report && !m_text_search );

  fclose (fp);

  return 1;
}

static char *
disbar (str)
     char *str;
{
  char *q;
  
  while ((q = strchr (str, '|')))
    *q = '!';

  return str;
}

void
print_pr (path, p, opened)
     char *path;
     char *p;
     int opened;
{
  /* Note: don't use P in any real way, cuz it could be "1234" or
     "category/1234".  */
  if (! opened && ! get_pr (path, p))
    return;

  if (full_report)
    {
      write_header (outfile, NUM_HEADER_ITEMS);
      fprintf (outfile, "\n");
      write_pr (outfile, NUM_PR_ITEMS);
    }
  else if (sql_format)
    {
      /* Trim `foo (Full Foo)' to just `foo'.  */
      char *q = (char *) strchr (pr[RESPONSIBLE].value, ' ');
      if (q != NULL)
	*q = '\0';

      fprintf (outfile, "%-8.8s|%-16.16s|%-128.128s|%-3.3s",
	       pr[NUMBER].value,
	       pr[CATEGORY].value,
	       disbar(pr[SYNOPSIS].value),
	       pr[CONFIDENTIAL].value);
      fprintf (outfile, "|%1.1d|%1.1d|%-16.16s|%1.1d|%1.1d|%-16.16s",
	       sql_types (pr[SEVERITY].value, Severity),
	       sql_types (pr[PRIORITY].value, Priority),
	       pr[RESPONSIBLE].value,
	       sql_types (pr[STATE].value, State),
	       sql_types (pr[CLASS].value, Class),
	       pr[SUBMITTER].value);
      fprintf (outfile, "|%-16.16s|%-64.64s|%-64.64s|",
	       sql_time (pr[ARRIVAL_DATE].value),
	       disbar(pr[ORIGINATOR].value),
	       pr[RELEASE].value);
    }
  else if (summary)
    {
      char *s;

      s = (char *) strchr (pr[RESPONSIBLE].value, ' ');
      if (s)
	*s = '\0';
      fprintf (outfile,
	       "%8s %-8.8s %-8.8s %-9.9s %-9.9s %-8.8s %-10.10s %s",
	       pr[NUMBER].value, pr[RESPONSIBLE].value,
	       pr[CATEGORY].value, pr[STATE].value, pr[SEVERITY].value,
	       pr[PRIORITY].value, pr[SUBMITTER].value,
	       pr[SYNOPSIS].value);
    }
  else
    {
      /* Print this out for emacs when people do `M-x query-pr' and want
	 to be able to use the next-error function on the buffer.  */
      if (print_path)
	fprintf (outfile, "%s:0:\n", path);

      write_pr (outfile, NUMBER);
      write_pr (outfile, CATEGORY);
      write_pr (outfile, SYNOPSIS);
      write_pr (outfile, CONFIDENTIAL);
      write_pr (outfile, SEVERITY);
      write_pr (outfile, PRIORITY);
      write_pr (outfile, RESPONSIBLE);
      write_pr (outfile, STATE);
      write_pr (outfile, CLASS);
      write_pr (outfile, SUBMITTER);
      write_pr (outfile, ORIGINATOR);
      write_pr (outfile, RELEASE);
      write_pr (outfile, ARRIVAL_DATE);
    }

  found_match = 1;
}

char *
make_path (c, n)
     char *c, *n;
{
  char *path = (char *) xmalloc (PATH_MAX);

  sprintf (path, "%s/%s/%s", gnats_root, c, n);
  return path;
}

static char case_fold[256];

/* Return 0 if matched, 1 otherwise. */
static int
regcmp (pat, match)
     char *pat, *match;
{
  struct re_pattern_buffer buf;
  union {
    const char *c;
    int i;
  } r;

  memset ((void *) &buf, 0, sizeof (buf));

  if (case_fold[1] == 0)
    {
      int i;
      for (i=0; i<256; i++)
	case_fold[i] = tolower(i);
    }
  buf.translate = case_fold;
#ifdef USE_RX
  buf.syntax_parens = 1;
#endif
  
  r.c = re_compile_pattern (pat, strlen (pat), &buf);
  if (r.c)
    {
      fprintf (stderr, "%s: re_compile_pattern: %s\n", program_name, r.c);
      exit (2);
    }
  r.i = re_match (&buf, match, strlen (match), 0, 0);
  regfree (&buf);
  switch (r.i)
    {
    case -2:
      fprintf (stderr, "%s: warning: re_match died on %s\n",
	       program_name, pr[NUMBER].value);
    case -1:
      return 1;
    default:
      return 0;
    }
}
      
/* Return 0 if found, 1 otherwise. */
static int
regfind (pat, match)
     char *pat, *match;
{
  struct re_pattern_buffer buf;
  union {
    const char *c;
    int i;
  } r;

  memset ((void *) &buf, 0, sizeof (buf));

  if (case_fold[1] == 0)
    {
      int i;
      for (i=0; i<256; i++)
	case_fold[i] = tolower(i);
    }
  buf.translate = case_fold;
  buf.fastmap = xmalloc (256);
#ifdef USE_RX
  buf.syntax_parens = 1;
#endif
  
  r.c = re_compile_pattern (pat, strlen (pat), &buf);
  if (r.c)
    {
      fprintf (stderr, "%s: re_compile_pattern: %s\n", program_name, r.c);
      exit (2);
    }
  r.i = strlen (match);
  r.i = re_search (&buf, match, r.i, 0, r.i, 0);
  regfree (&buf);
  switch (r.i)
    {
    case -2:
      fprintf (stderr, "%s: warning: re_search died on %s\n",
	       program_name, pr[NUMBER].value);
    case -1:
      return 1;
    default:
      return 0;
    }
}
      
static int
pr_matches (s, i)
     Index *s;
     Index *i;
{
  if (skip_closed && strcmp (i->state, "closed") == 0) return 0;
  if (!s || !searching) return 1;
  return (!s->category || (regcmp (s->category, i->category) == 0))
    && (!s->submitter || (regcmp (s->submitter, i->submitter) == 0))
    && (!s->responsible || (regcmp (s->responsible, i->responsible) == 0))
    && (!s->state || (regcmp (s->state, i->state) == 0))
    && (!s->confidential || (regcmp (s->confidential, i->confidential) == 0))
    && (!s->severity || (regcmp (s->severity, i->severity) == 0))
    && (!s->priority || (regcmp (s->priority, i->priority) == 0));
}

int
check_text ()
{
  PR_Name i;

  if (text_search)
    {
      for (i = NUMBER; i < NUM_PR_ITEMS; i++)
	{
	  if (pr[i].datatype != Text)
	    continue;
	  
	  if (pr[i].value && regfind (text_search, pr[i].value) == 0)
	    goto part2;
	}
      return 0;
    }
 part2:
  if (m_text_search)
    {
      for (i = NUMBER; i < NUM_PR_ITEMS; i++)
	{
	  if (pr[i].datatype != MultiText)
	    continue;
	  
	  if (pr[i].value && regfind (m_text_search, pr[i].value) == 0)
	    return 1;
	}
      return 0;
    }
  return 1;
}

/* Possibly print out a PR, checking if we want to verify the originator
   first.  */
int
do_pr_internal (path, prnum)
     char *path, *prnum;
{
  if (originator || text_search || m_text_search)
    {
      if (! get_pr (path, prnum))
	return 0;

      if ((originator && (pr[ORIGINATOR].value == NULL
			 || regcmp (originator, pr[ORIGINATOR].value) != 0))
	  || !check_text())
	return 0;
      return 1;
    }
  else
    return get_pr (path, prnum);
}

int
do_pr (i)
     Index *i;
{
  int val = 0;
  char *path = make_path (i->category, i->number);

  if ((val = do_pr_internal (path, i->number)))
    print_pr (path, i->number, 1);
  
  free (path);

  return val;
}

int
numeric (p)
     char *p;
{
  int i;
  int l = strlen(p);
  for (i=0; i<l; i++)
    {
      if (!isdigit (p[i]))
	return 0;
    }
  return 1;
}

int
query_pr (p, s)
     char *p;
     Index *s;
{
  char *path;
  Index *i;

  if (p == NULL)
    {
      /* We weren't given a list of PRs to check, so we do the
	 whole shooting match.  */
      for (i = index_chain; i ; i = i->next)
	if (pr_matches (s, i) && do_pr (i))
	  fprintf (outfile, "\n");
    }
  else
    {
      if (searching || !numeric (p))
	{
	  char pat[40];
	  strcpy (pat, p);
	  strcat (pat, "\\'");

	  /* Search in the problem report P for a satisfying condition. :) */
	  if (numeric (p))	/* unswitching */
	    {
	      for (i = index_chain; i ; i = i->next)
		if (regcmp (pat, i->number) == 0)
		  {
		    if (pr_matches (s, i)
			&& do_pr (i))
		      fprintf (outfile, "\n");
		    break;
		  }
	    }
	  else
	    {
	      for (i = index_chain; i ; i = i->next)
		if ((regcmp (pat, i->number) == 0)
		    && pr_matches (s, i)
		    && do_pr (i))
		  fprintf (outfile, "\n");
	    }
	}
      else
	{
	  /* They did a plain query-pr with a list of PRs, just print
	     this one out.  */
	  if (((char *) strchr (p, '/')) == NULL)
	    path = get_category (p);
	  else
	    {
	      path = (char *) xmalloc (PATH_MAX);
	      sprintf (path, "%s/%s", gnats_root, p);
	    }

	  if (path)
	    {
	      if (get_pr (path, p)
		  && ! (skip_closed
			&& strcmp (pr[STATE].value, "closed") == 0)
		  && do_pr_internal (path, p))
		{
		  print_pr (path, p, 1);
		  fprintf (outfile, "\n");
		}		  

	      free (path);
	    }
	  else
	    {
	      fprintf (stderr, "%s: couldn't find PR %s\n",
		       program_name, p);
	      return 1;
	    }
	}
    }

  return 0;
}

void
main (argc, argv)
     int argc;
     char **argv;
{
  int optc;
  Index *s = (Index *) xmalloc (sizeof (Index));
  int errors = 0;
  char *spec_gnats_root = 0;

  program_name = basename (argv[0]);

  memset (s, 0, sizeof (Index));

  while ((optc = getopt_long (argc, argv, "c:C:d:e:m:o:O:p:Ps:S:r:t:VFixhqR",
			      long_options, (int *) 0)) != EOF)
    {
      switch (optc)
	{
	case 'd':
	  spec_gnats_root = optarg;
	  break;

	case 'o':
	  if (strcmp (optarg, "-") && !restricted)
	    {
	      outfile = fopen (optarg, "w+");
	      if (outfile == (FILE *) NULL)
		{
		  fprintf (stderr, "can not read file %s", optarg);
		  exit (3);
		}
	    }
	  break;

	case 'r':
	  s->responsible = optarg;
	  searching = 1;
	  break;

	case 'R':
	  restricted = 1;
	  outfile = stdout;
	  s->confidential = "no";
	  searching = 1;
	  break;

	case 'c':
	  s->category = optarg;
	  searching = 1;
	  break;

	case 'C':
	  if (!restricted) s->confidential = optarg;
	  searching = 1;
	  break;

	case 'e':
	  s->severity = optarg;
	  searching = 1;
	  break;

	case 'm':
	  m_text_search = optarg;
	  break;

	case 'O':
	  originator = optarg;
	  break;

	case 'p':
	  s->priority = optarg;
	  searching = 1;
	  break;

	case 'P':
	  print_path = 1;
	  break;

	case 's':
	  s->state = optarg;
	  searching = 1;
	  break;

	case 'S':
	  s->submitter = optarg;
	  searching = 1;
	  break;

	case 't':
	  text_search = optarg;
	  break;

	case 'V':
	  version ();
	  exit (0);

	case 'F':
	  full_report = 1;
	  break;

	case 'i':
	  sql_format = 1;
	  break;

	case 'x':
	  skip_closed = 1;
	  break;

	case 'q':
	  summary = 1;
	  break;

	case 'h':
	  usage (0);
	  break;

	default:
	  usage (1);
	}
    }

  if (spec_gnats_root && !restricted)
    gnats_root = spec_gnats_root;

  if ((sql_format + full_report + summary) > 1)
    {
      fprintf (stderr, "%s: only one of -i, -F, or -q may be specified\n",
	       program_name);
      exit (3);
    }

  configure ();
  init_gnats ();
  re_set_syntax ((RE_SYNTAX_POSIX_EXTENDED | RE_BK_PLUS_QM) & ~RE_DOT_NEWLINE);

  index_chain = get_index ();

  if (optind == argc)
    {
      if (! searching)
	errors += query_pr ((char *) NULL, (Index *) NULL);
      else
	errors += query_pr ((char *) NULL, s);
    }
  else
    {
      while (optind < argc)
	{
	  found_match = 0;
	  errors += query_pr (argv[optind++], searching ? s : (Index *) NULL);
	}
    }

  /* Exit non-zero if there were any troubles.  */
  exit (errors != 0);
}

void
usage (stat)
     int stat;
{
  fprintf (stderr, "\
Usage: %s [-FhiPqVx] [-C confidential] [-c category] [-d directory]\n\
       [-e severity] [-m mtext] [-O originator] [-o outfile] [-p priority]\n\
       [-r responsible] [-S submitter] [-s state] [-t text] [--full] \n\
       [--help] [--sql] [--print-path] [--summary] [--version]\n\
       [--skip-closed] [--category=category] [--confidential=yes|no]\n\
       [--directory=directory] [--output=outfile] [--originator=name]\n\
       [--priority=level] [--responsible=person] [--severity=severity]\n\
       [--state=state] [--submitter=submitter] [--text=text]\n\
       [--multitext=mtext] [PR] [PR]...\n",
	   program_name);
  exit (stat);
}

void
version ()
{
  printf ("query-pr %s\n", version_string);
}
