/* indices.c -- deal with an Info file index.
   $Id: indices.c,v 1.14 1999/09/25 16:10:04 karl Exp $

   Copyright (C) 1993, 97, 98, 99 Free Software Foundation, Inc.

   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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

   Written by Brian Fox (bfox@ai.mit.edu). */

#include "info.h"
#include "indices.h"

/* User-visible variable controls the output of info-index-next. */
int show_index_match = 1;

/* In the Info sense, an index is a menu.  This variable holds the last
   parsed index. */
static REFERENCE **index_index = (REFERENCE **)NULL;

/* The offset of the most recently selected index element. */
static int index_offset = 0;

/* Variable which holds the last string searched for. */
static char *index_search = (char *)NULL;

/* A couple of "globals" describing where the initial index was found. */
static char *initial_index_filename = (char *)NULL;
static char *initial_index_nodename = (char *)NULL;

/* A structure associating index names with index offset ranges. */
typedef struct {
  char *name;                   /* The nodename of this index. */
  int first;                    /* The index in our list of the first entry. */
  int last;                     /* The index in our list of the last entry. */
} INDEX_NAME_ASSOC;

/* An array associating index nodenames with index offset ranges. */
static INDEX_NAME_ASSOC **index_nodenames = (INDEX_NAME_ASSOC **)NULL;
static int index_nodenames_index = 0;
static int index_nodenames_slots = 0;

/* Add the name of NODE, and the range of the associated index elements
   (passed in ARRAY) to index_nodenames. */
static void
add_index_to_index_nodenames (array, node)
     REFERENCE **array;
     NODE *node;
{
  register int i, last;
  INDEX_NAME_ASSOC *assoc;

  for (last = 0; array[last]; last++);
  assoc = (INDEX_NAME_ASSOC *)xmalloc (sizeof (INDEX_NAME_ASSOC));
  assoc->name = xstrdup (node->nodename);

  if (!index_nodenames_index)
    {
      assoc->first = 0;
      assoc->last = last;
    }
  else
    {
      for (i = 0; index_nodenames[i + 1]; i++);
      assoc->first = 1 + index_nodenames[i]->last;
      assoc->last = assoc->first + last;
    }
  add_pointer_to_array
    (assoc, index_nodenames_index, index_nodenames, index_nodenames_slots,
     10, INDEX_NAME_ASSOC *);
}

/* Find and return the indices of WINDOW's file.  The indices are defined
   as the first node in the file containing the word "Index" and any
   immediately following nodes whose names also contain "Index".  All such
   indices are concatenated and the result returned.  If WINDOW's info file
   doesn't have any indices, a NULL pointer is returned. */
REFERENCE **
info_indices_of_window (window)
     WINDOW *window;
{
  FILE_BUFFER *fb;

  fb = file_buffer_of_window (window);

  return (info_indices_of_file_buffer (fb));
}

REFERENCE **
info_indices_of_file_buffer (file_buffer)
     FILE_BUFFER *file_buffer;
{
  register int i;
  REFERENCE **result = (REFERENCE **)NULL;

  /* No file buffer, no indices. */
  if (!file_buffer)
    return ((REFERENCE **)NULL);

  /* Reset globals describing where the index was found. */
  maybe_free (initial_index_filename);
  maybe_free (initial_index_nodename);
  initial_index_filename = (char *)NULL;
  initial_index_nodename = (char *)NULL;

  if (index_nodenames)
    {
      for (i = 0; index_nodenames[i]; i++)
        {
          free (index_nodenames[i]->name);
          free (index_nodenames[i]);
        }

      index_nodenames_index = 0;
      index_nodenames[0] = (INDEX_NAME_ASSOC *)NULL;
    }

  /* Grovel the names of the nodes found in this file. */
  if (file_buffer->tags)
    {
      TAG *tag;

      for (i = 0; (tag = file_buffer->tags[i]); i++)
        {
          if (string_in_line ("Index", tag->nodename) != -1)
            {
              NODE *node;
              REFERENCE **menu;

              /* Found one.  Get its menu. */
              node = info_get_node (tag->filename, tag->nodename);
              if (!node)
                continue;

              /* Remember the filename and nodename of this index. */
              initial_index_filename = xstrdup (file_buffer->filename);
              initial_index_nodename = xstrdup (tag->nodename);

              menu = info_menu_of_node (node);

              /* If we have a menu, add this index's nodename and range
                 to our list of index_nodenames. */
              if (menu)
                {
                  add_index_to_index_nodenames (menu, node);

                  /* Concatenate the references found so far. */
                  result = info_concatenate_references (result, menu);
                }
              free (node);
            }
        }
    }

  /* If there is a result, clean it up so that every entry has a filename. */
  for (i = 0; result && result[i]; i++)
    if (!result[i]->filename)
      result[i]->filename = xstrdup (file_buffer->filename);

  return (result);
}

DECLARE_INFO_COMMAND (info_index_search,
   _("Look up a string in the index for this file"))
{
  do_info_index_search (window, count, 0);
}

/* Look up SEARCH_STRING in the index for this file.  If SEARCH_STRING
   is NULL, prompt user for input.  */ 
void
do_info_index_search (window, count, search_string)
     WINDOW *window;
     int count;
     char *search_string;
{
  FILE_BUFFER *fb;
  char *line;

  /* Reset the index offset, since this is not the info-index-next command. */
  index_offset = 0;

  /* The user is selecting a new search string, so flush the old one. */
  maybe_free (index_search);
  index_search = (char *)NULL;

  /* If this window's file is not the same as the one that we last built an
     index for, build and remember an index now. */
  fb = file_buffer_of_window (window);
  if (!initial_index_filename ||
      (FILENAME_CMP (initial_index_filename, fb->filename) != 0))
    {
      info_free_references (index_index);
      window_message_in_echo_area (_("Finding index entries..."));
      index_index = info_indices_of_file_buffer (fb);
    }

  /* If there is no index, quit now. */
  if (!index_index)
    {
      info_error (_("No indices found."));
      return;
    }

  /* Okay, there is an index.  Look for SEARCH_STRING, or, if it is
     empty, prompt for one.  */
  if (search_string && *search_string)
    line = xstrdup (search_string);
  else
    {
      line = info_read_maybe_completing (window, _("Index entry: "),
                                         index_index);
      window = active_window;

      /* User aborted? */
      if (!line)
        {
          info_abort_key (active_window, 1, 0);
          return;
        }

      /* Empty line means move to the Index node. */
      if (!*line)
        {
          free (line);

          if (initial_index_filename && initial_index_nodename)
            {
              NODE *node;

              node = info_get_node (initial_index_filename,
                                    initial_index_nodename);
              set_remembered_pagetop_and_point (window);
              window_set_node_of_window (window, node);
              remember_window_and_node (window, node);
              window_clear_echo_area ();
              return;
            }
        }
    }

  /* The user typed either a completed index label, or a partial string.
     Find an exact match, or, failing that, the first index entry containing
     the partial string.  So, we just call info_next_index_match () with minor
     manipulation of INDEX_OFFSET. */
  {
    int old_offset;

    /* Start the search right after/before this index. */
    if (count < 0)
      {
        register int i;
        for (i = 0; index_index[i]; i++);
        index_offset = i;
      }
    else
      index_offset = -1;

    old_offset = index_offset;

    /* The "last" string searched for is this one. */
    index_search = line;

    /* Find it, or error. */
    info_next_index_match (window, count, 0);

    /* If the search failed, return the index offset to where it belongs. */
    if (index_offset == old_offset)
      index_offset = 0;
  }
}

int
index_entry_exists (window, string)
     WINDOW *window;
     char *string;
{
  register int i;
  FILE_BUFFER *fb;

  /* If there is no previous search string, the user hasn't built an index
     yet. */
  if (!string)
    return 0;

  fb = file_buffer_of_window (window);
  if (!initial_index_filename
      || (FILENAME_CMP (initial_index_filename, fb->filename) != 0))
    {
      info_free_references (index_index);
      index_index = info_indices_of_file_buffer (fb);
    }

  /* If there is no index, that is an error. */
  if (!index_index)
    return 0;

  for (i = 0; (i > -1) && (index_index[i]); i++)
    if (strcmp (string, index_index[i]->label) == 0)
      break;

  /* If that failed, look for the next substring match. */
  if ((i < 0) || (!index_index[i]))
    {
      for (i = 0; (i > -1) && (index_index[i]); i++)
        if (string_in_line (string, index_index[i]->label) != -1)
          break;

      if ((i > -1) && (index_index[i]))
        string_in_line (string, index_index[i]->label);
    }

  /* If that failed, return 0. */
  if ((i < 0) || (!index_index[i]))
    return 0;

  return 1;
}

DECLARE_INFO_COMMAND (info_next_index_match,
 _("Go to the next matching index item from the last `\\[index-search]' command"))
{
  register int i;
  int partial, dir;
  NODE *node;

  /* If there is no previous search string, the user hasn't built an index
     yet. */
  if (!index_search)
    {
      info_error (_("No previous index search string."));
      return;
    }

  /* If there is no index, that is an error. */
  if (!index_index)
    {
      info_error (_("No index entries."));
      return;
    }

  /* The direction of this search is controlled by the value of the
     numeric argument. */
  if (count < 0)
    dir = -1;
  else
    dir = 1;

  /* Search for the next occurence of index_search.  First try to find
     an exact match. */
  partial = 0;

  for (i = index_offset + dir; (i > -1) && (index_index[i]); i += dir)
    if (strcmp (index_search, index_index[i]->label) == 0)
      break;

  /* If that failed, look for the next substring match. */
  if ((i < 0) || (!index_index[i]))
    {
      for (i = index_offset + dir; (i > -1) && (index_index[i]); i += dir)
        if (string_in_line (index_search, index_index[i]->label) != -1)
          break;

      if ((i > -1) && (index_index[i]))
        partial = string_in_line (index_search, index_index[i]->label);
    }

  /* If that failed, print an error. */
  if ((i < 0) || (!index_index[i]))
    {
      info_error (_("No %sindex entries containing \"%s\"."),
                  index_offset > 0 ? _("more ") : "", index_search);
      return;
    }

  /* Okay, we found the next one.  Move the offset to the current entry. */
  index_offset = i;

  /* Report to the user on what we have found. */
  {
    register int j;
    char *name = _("CAN'T SEE THIS");
    char *match;

    for (j = 0; index_nodenames[j]; j++)
      {
        if ((i >= index_nodenames[j]->first) &&
            (i <= index_nodenames[j]->last))
          {
            name = index_nodenames[j]->name;
            break;
          }
      }

    /* If we had a partial match, indicate to the user which part of the
       string matched. */
    match = xstrdup (index_index[i]->label);

    if (partial && show_index_match)
      {
        int j, ls, start, upper;

        ls = strlen (index_search);
        start = partial - ls;
        upper = isupper (match[start]) ? 1 : 0;

        for (j = 0; j < ls; j++)
          if (upper)
            match[j + start] = info_tolower (match[j + start]);
          else
            match[j + start] = info_toupper (match[j + start]);
      }

    {
      char *format;

      format = replace_in_documentation
        (_("Found \"%s\" in %s. (`\\[next-index-match]' tries to find next.)"));

      window_message_in_echo_area (format, match, name);
    }

    free (match);
  }

  /* Select the node corresponding to this index entry. */
  node = info_get_node (index_index[i]->filename, index_index[i]->nodename);

  if (!node)
    {
      info_error (msg_cant_file_node,
                  index_index[i]->filename, index_index[i]->nodename);
      return;
    }

  info_set_node_of_window (1, window, node);

  /* Try to find an occurence of LABEL in this node. */
  {
    long start, loc;

    start = window->line_starts[1] - window->node->contents;
    loc = info_target_search_node (node, index_index[i]->label, start);

    if (loc != -1)
      {
        window->point = loc;
        window_adjust_pagetop (window);
      }
  }
}

/* **************************************************************** */
/*                                                                  */
/*                 Info APROPOS: Search every known index.          */
/*                                                                  */
/* **************************************************************** */

/* For every menu item in DIR, search the indices of that file for
   SEARCH_STRING. */
REFERENCE **
apropos_in_all_indices (search_string, inform)
     char *search_string;
     int inform;
{
  register int i, dir_index;
  REFERENCE **all_indices = (REFERENCE **)NULL;
  REFERENCE **dir_menu = (REFERENCE **)NULL;
  NODE *dir_node;

  dir_node = info_get_node ("dir", "Top");
  if (dir_node)
    dir_menu = info_menu_of_node (dir_node);

  if (!dir_menu)
    return NULL;

  /* For every menu item in DIR, get the associated node's file buffer and
     read the indices of that file buffer.  Gather all of the indices into
     one large one. */
  for (dir_index = 0; dir_menu[dir_index]; dir_index++)
    {
      REFERENCE **this_index, *this_item;
      NODE *this_node;
      FILE_BUFFER *this_fb;
      int dir_node_duplicated = 0;

      this_item = dir_menu[dir_index];

      if (!this_item->filename)
        {
	  dir_node_duplicated = 1;
          if (dir_node->parent)
            this_item->filename = xstrdup (dir_node->parent);
          else
            this_item->filename = xstrdup (dir_node->filename);
        }

      /* Find this node.  If we cannot find it, try using the label of the
         entry as a file (i.e., "(LABEL)Top"). */
      this_node = info_get_node (this_item->filename, this_item->nodename);

      if (!this_node && this_item->nodename &&
          (strcmp (this_item->label, this_item->nodename) == 0))
        this_node = info_get_node (this_item->label, "Top");

      if (!this_node)
	{
	  if (dir_node_duplicated)
	    free (this_item->filename);
	  continue;
	}

      /* Get the file buffer associated with this node. */
      {
        char *files_name;

        files_name = this_node->parent;
        if (!files_name)
          files_name = this_node->filename;

        this_fb = info_find_file (files_name);

	/* If we already scanned this file, don't do that again.
	   In addition to being faster, this also avoids having
	   multiple identical entries in the *Apropos* menu.  */
	for (i = 0; i < dir_index; i++)
	  if (FILENAME_CMP (this_fb->filename, dir_menu[i]->filename) == 0)
	    break;
	if (i < dir_index)
	  {
	    if (dir_node_duplicated)
	      free (this_item->filename);
	    continue;
	  }

        if (this_fb && inform)
          message_in_echo_area (_("Scanning indices of \"%s\"..."), files_name);

        this_index = info_indices_of_file_buffer (this_fb);
        free (this_node);

        if (this_fb && inform)
          unmessage_in_echo_area ();
      }

      if (this_index)
        {
          /* Remember the filename which contains this set of references. */
          for (i = 0; this_index && this_index[i]; i++)
            if (!this_index[i]->filename)
              this_index[i]->filename = xstrdup (this_fb->filename);

          /* Concatenate with the other indices.  */
          all_indices = info_concatenate_references (all_indices, this_index);
        }
    }

  info_free_references (dir_menu);

  /* Build a list of the references which contain SEARCH_STRING. */
  if (all_indices)
    {
      REFERENCE *entry, **apropos_list = (REFERENCE **)NULL;
      int apropos_list_index = 0;
      int apropos_list_slots = 0;

      for (i = 0; (entry = all_indices[i]); i++)
        {
          if (string_in_line (search_string, entry->label) != -1)
            {
              add_pointer_to_array
                (entry, apropos_list_index, apropos_list, apropos_list_slots,
                 100, REFERENCE *);
            }
          else
            {
              maybe_free (entry->label);
              maybe_free (entry->filename);
              maybe_free (entry->nodename);
              free (entry);
            }
        }

      free (all_indices);
      all_indices = apropos_list;
    }
  return (all_indices);
}

#define APROPOS_NONE \
   N_("No available info files have \"%s\" in their indices.")

void
info_apropos (string)
     char *string;
{
  REFERENCE **apropos_list;

  apropos_list = apropos_in_all_indices (string, 0);

  if (!apropos_list)
    {
      info_error (_(APROPOS_NONE), string);
    }
  else
    {
      register int i;
      REFERENCE *entry;

      for (i = 0; (entry = apropos_list[i]); i++)
        fprintf (stdout, "\"(%s)%s\" -- %s\n",
                 entry->filename, entry->nodename, entry->label);
    }
  info_free_references (apropos_list);
}

static char *apropos_list_nodename = "*Apropos*";

DECLARE_INFO_COMMAND (info_index_apropos,
   _("Grovel all known info file's indices for a string and build a menu"))
{
  char *line;

  line = info_read_in_echo_area (window, _("Index apropos: "));

  window = active_window;

  /* User aborted? */
  if (!line)
    {
      info_abort_key (window, 1, 1);
      return;
    }

  /* User typed something? */
  if (*line)
    {
      REFERENCE **apropos_list;
      NODE *apropos_node;

      apropos_list = apropos_in_all_indices (line, 1);

      if (!apropos_list)
        {
          info_error (_(APROPOS_NONE), line);
        }
      else
        {
          register int i;
          char *line_buffer;

          initialize_message_buffer ();
          printf_to_message_buffer
            (_("\n* Menu: Nodes whoses indices contain \"%s\":\n"), line);
          line_buffer = (char *)xmalloc (500);

          for (i = 0; apropos_list[i]; i++)
            {
              int len;
	      /* The label might be identical to that of another index
		 entry in another Info file.  Therefore, we make the file
		 name part of the menu entry, to make them all distinct.  */
              sprintf (line_buffer, "* %s [%s]: ",
		       apropos_list[i]->label, apropos_list[i]->filename);
              len = pad_to (40, line_buffer);
              sprintf (line_buffer + len, "(%s)%s.",
                       apropos_list[i]->filename, apropos_list[i]->nodename);
              printf_to_message_buffer ("%s\n", line_buffer);
            }
          free (line_buffer);
        }

      apropos_node = message_buffer_to_node ();
      add_gcable_pointer (apropos_node->contents);
      name_internal_node (apropos_node, apropos_list_nodename);

      /* Even though this is an internal node, we don't want the window
         system to treat it specially.  So we turn off the internalness
         of it here. */
      apropos_node->flags &= ~N_IsInternal;

      /* Find/Create a window to contain this node. */
      {
        WINDOW *new;
        NODE *node;

        set_remembered_pagetop_and_point (window);

        /* If a window is visible and showing an apropos list already,
           re-use it. */
        for (new = windows; new; new = new->next)
          {
            node = new->node;

            if (internal_info_node_p (node) &&
                (strcmp (node->nodename, apropos_list_nodename) == 0))
              break;
          }

        /* If we couldn't find an existing window, try to use the next window
           in the chain. */
        if (!new && window->next)
          new = window->next;

        /* If we still don't have a window, make a new one to contain
           the list. */
        if (!new)
          {
            WINDOW *old_active;

            old_active = active_window;
            active_window = window;
            new = window_make_window ((NODE *)NULL);
            active_window = old_active;
          }

        /* If we couldn't make a new window, use this one. */
        if (!new)
          new = window;

        /* Lines do not wrap in this window. */
        new->flags |= W_NoWrap;

        window_set_node_of_window (new, apropos_node);
        remember_window_and_node (new, apropos_node);
        active_window = new;
      }
      info_free_references (apropos_list);
    }
  free (line);

  if (!info_error_was_printed)
    window_clear_echo_area ();
}

