/* pathfinder.h: find one of multiple file names in path list

This file is part of Cygwin.

This software is a copyrighted work licensed under the terms of the
Cygwin license.  Please consult the file "CYGWIN_LICENSE" for
details. */

#include "vstrlist.h"

#ifdef __cplusplus

/* Search a list of directory names for first occurrence of a file,
   which's file name matches one out of a list of file names.  */
class pathfinder
{
public:
  typedef vstrlist searchdirlist;
  typedef vstrlist basenamelist;

private:
  pathfinder ();
  pathfinder (pathfinder const &);
  pathfinder & operator = (pathfinder const &);

  basenamelist basenames_;
  size_t       basenames_maxlen_;

  /* Add to searchdirs_ with extra buffer for any basename we may search for.
     This is an optimization for the loops in check_path_access method. */
  searchdirlist searchdirs_;

public:
  ~pathfinder () {}

  /* We need the basenames to search for first, to allow for optimized
     memory allocation of each searchpath + longest basename combination.
     The incoming list of basenames is emptied (ownership take over). */
  pathfinder (allocator_interface & a, basenamelist & basenames)
    : basenames_ (a)
    , basenames_maxlen_ ()
    , searchdirs_(a)
  {
    basenames_.swap(basenames);

    for (basenamelist::buffer_iterator basename (basenames_.begin ());
	 basename != basenames_.end ();
	 ++ basename)
      {
	if (basenames_maxlen_ < basename->bufferlength ())
	  basenames_maxlen_ = basename->bufferlength ();
      }
  }

  void add_searchdir (const char *dir, int dirlen)
  {
      if (dirlen < 0)
	dirlen = strlen (dir);

      if (!dirlen)
	return;

      searchdirs_.appendv (dir, dirlen, "/", 1 + basenames_maxlen_, NULL);
  }

  void add_searchpath (const char *path)
  {
    while (path && *path)
      {
	const char *next = strchr (path, ':');
	add_searchdir (path, next ? next - path : -1);
	path = next ? next + 1 : next;
      }
  }

  void add_envsearchpath (const char *envpath)
    {
      add_searchpath (getenv (envpath));
    }


  /* pathfinder::criterion_interface
     Overload this test method when you need separate dir and basename.  */
  struct criterion_interface
  {
    virtual char const * name () const { return NULL; }

    virtual bool test (searchdirlist::iterator dir,
		       basenamelist::iterator name) const = 0;
  };


  /* pathfinder::simple_criterion_interface
     Overload this test method when you need a single filename.  */
  class simple_criterion_interface
    : public criterion_interface
  {
    virtual bool test (searchdirlist::iterator dir,
		       basenamelist::iterator name) const
    {
      /* Complete the filename path to search for within dir,
	 We have allocated enough memory above.  */
      searchdirlist::buffer_iterator dirbuf (dir);
      memcpy (dirbuf->buffer () + dirbuf->stringlength (),
	      name->string (), name->stringlength () + 1);
      bool ret = testfile (dirbuf->string ());
      /* reset original dir */
      dirbuf->buffer ()[dirbuf->stringlength ()] = '\0';
      return ret;
    }

  public:
    virtual bool testfile (const char * filename) const = 0;
  };


  /* pathfinder::path_conv_criterion_interface
     Overload this test method when you need a path_conv. */
  class path_conv_criterion_interface
    : public simple_criterion_interface
  {
    path_conv mypc_;
    path_conv & pc_;
    unsigned opt_;

    /* simple_criterion_interface */
    virtual bool testfile (const char * filename) const
    {
      pc_.check (filename, opt_);
      return testpc (pc_);
    }

  public:
    path_conv_criterion_interface (unsigned opt = PC_SYM_FOLLOW)
      : mypc_ ()
      , pc_ (mypc_)
      , opt_ (opt)
    {}

    path_conv_criterion_interface (path_conv & ret, unsigned opt = PC_SYM_FOLLOW)
      : mypc_ ()
      , pc_ (ret)
      , opt_ (opt)
    {}

    virtual bool testpc (path_conv & pc) const = 0;
  };


  /* pathfinder::exists_and_not_dir
     Test if path_conv argument does exist and is not a directory. */
  struct exists_and_not_dir
    : public path_conv_criterion_interface
  {
    virtual char const * name () const { return "exists and not dir"; }

    exists_and_not_dir (path_conv & pc, unsigned opt = PC_SYM_FOLLOW)
      : path_conv_criterion_interface (pc, opt)
    {}

    /* path_conv_criterion_interface */
    virtual bool testpc (path_conv & pc) const
    {
      if (pc.exists () && !pc.isdir ())
	return true;

      pc.error = ENOENT;
      return false;
    }
  };


  /* Find the single dir + basename that matches criterion.

     Calls criterion.test method for each registered dir + basename
     until returning true:
       Returns true with found_dir + found_basename set.
     If criterion.test method never returns true:
       Returns false, not modifying found_dir nor found_basename.  */
  bool find (criterion_interface const & criterion,
	     searchdirlist::member const ** found_dir = NULL,
	     basenamelist::member const ** found_basename = NULL)
  {
    char const * critname = criterion.name ();
    for (searchdirlist::iterator dir(searchdirs_.begin ());
	 dir != searchdirs_.end ();
	 ++dir)
      for (basenamelist::iterator name = basenames_.begin ();
	   name != basenames_.end ();
	   ++name)
	if (criterion.test (dir, name))
	  {
	    debug_printf ("(%s), take %s%s", critname,
			  dir->string(), name->string ());
	    if (found_dir)
	      *found_dir = dir.operator -> ();
	    if (found_basename)
	      *found_basename = name.operator -> ();
	    return true;
	  }
	else
	  debug_printf ("not (%s), skip %s%s", critname,
			dir->string(), name->string ());
    return false;
  }
};

#endif /* __cplusplus */
