/*
 * Copyright (c) 1992, Brian Berliner and Jeff Polk
 * Copyright (c) 1989-1992, Brian Berliner
 * Copyright (c) 2001, Tony Hoyle
 * Copyright (c) 2004, Derek R. Price & Ximbiot <http://ximbiot.com>
 *
 * You may distribute under the terms of the GNU General Public License as
 * specified in the README file that comes with the CVS source distribution.
 *
 * Query CVS/Entries from server
 */

#include "cvs.h"
#include <stdbool.h>

static int ls_proc (int argc, char **argv, char *xwhere, char *mwhere,
                    char *mfile, int shorten, int local, char *mname,
                    char *msg);

static const char *const ls_usage[] =
{
    "Usage: %s %s [-e | -l] [-RP] [-r rev] [-D date] [path...]\n",
    "\t-d\tShow dead revisions (with tag when specified).\n",
    "\t-e\tDisplay in CVS/Entries format.\n",
    "\t-l\tDisplay all details.\n",
    "\t-P\tPrune empty directories.\n",
    "\t-R\tList recursively.\n",
    "\t-r rev\tShow files with revision or tag.\n",
    "\t-D date\tShow files from date.\n",
    "(Specify the --help global option for a list of other help options)\n",
    NULL
};

static bool entries_format;
static bool long_format;
static char *show_tag;
static char *show_date;
static bool set_tag;
static char *created_dir;
static bool tag_validated;
static bool recurse;
static bool ls_prune_dirs;
static char *regexp_match;
static bool is_rls;
static bool show_dead_revs;



int
ls (int argc, char **argv)
{
    int c;
    int err = 0;

    is_rls = strcmp (cvs_cmd_name, "rls") == 0;

    if (argc == -1)
	usage (ls_usage);

    entries_format = false;
    long_format = false;
    show_tag = NULL;
    show_date = NULL;
    tag_validated = false;
    recurse = false;
    ls_prune_dirs = false;
    show_dead_revs = false;

    optind = 0;

    while ((c = getopt (argc, argv,
#ifdef SERVER_SUPPORT
           server_active ? "qdelr:D:PR" :
#endif /* SERVER_SUPPORT */
           "delr:D:RP"
           )) != -1)
    {
	switch (c)
	{
#ifdef SERVER_SUPPORT
	    case 'q':
		if (server_active)
		{
		    error (0, 0,
"`%s ls -q' is deprecated.  Please use the global `-q' option instead.",
                           program_name);
		    quiet = true;
		}
		else
		    usage (ls_usage);
		break;
#endif /* SERVER_SUPPORT */
	    case 'd':
		show_dead_revs = true;
		break;
	    case 'e':
		entries_format = true;
		break;
	    case 'l':
		long_format = true;
		break;
	    case 'r':
		parse_tagdate (&show_tag, &show_date, optarg);
		break;
	    case 'D':
		if (show_date) free (show_date);
		show_date = Make_Date (optarg);
		break;
	    case 'P':
		ls_prune_dirs = true;
		break;
	    case 'R':
		recurse = true;
		break;
	    case '?':
	    default:
		usage (ls_usage);
		break;
	}
    }
    argc -= optind;
    argv += optind;

    if (entries_format && long_format)
    {
        error (0, 0, "`-e' & `-l' are mutually exclusive.");
        usage (ls_usage);
    }

    wrap_setup ();

#ifdef CLIENT_SUPPORT
    if (current_parsed_root->isremote)
    {
	/* We're the local client.  Fire up the remote server.	*/
	start_server ();

	ign_setup ();

	if (is_rls ? !(supported_request ("rlist") || supported_request ("ls"))
                   : !supported_request ("list"))
	    error (1, 0, "server does not support %s", cvs_cmd_name);

	if (quiet && !supported_request ("global-list-quiet"))
	    send_arg ("-q");
	if (entries_format)
	    send_arg ("-e");
	if (long_format)
	    send_arg ("-l");
	if (ls_prune_dirs)
	    send_arg ("-P");
	if (recurse)
	    send_arg ("-R");
	if (show_dead_revs)
	    send_arg ("-d");
	if (show_tag)
	    option_with_arg ("-r", show_tag);
	if (show_date)
	    client_senddate (show_date);

	send_arg ("--");

	if (is_rls)
	{
	    int i;
	    for (i = 0; i < argc; i++)
		send_arg (argv[i]);
            if (supported_request ("rlist"))
		send_to_server ("rlist\012", 0);
	    else
		/* For backwards compatibility with CVSNT...  */
		send_to_server ("ls\012", 0);
	}
	else
	{
	    /* Setting this means, I think, that any empty directories created
	     * by the server will be deleted by the client.  Since any dirs
	     * created at all by ls should remain empty, this should cause any
	     * dirs created by the server for the ls command to be deleted.
	     */
	    client_prune_dirs = 1;

	    /* I explicitly decide not to send contents here.  We *could* let
	     * the user pull status information with this command, but why
	     * don't they just use update or status?
	     */
	    send_files (argc, argv, !recurse, 0, SEND_NO_CONTENTS);
	    send_file_names (argc, argv, SEND_EXPAND_WILD);
	    send_to_server ("list\012", 0);
	}

	err = get_responses_and_close ();
	return err;
    }
#endif

    if (is_rls)
    {
	DBM *db;
	int i;
	db = open_module ();
	if (argc)
	{
	    for (i = 0; i < argc; i++)
	    {
		char *mod = xstrdup (argv[i]);
		char *p;

		for (p=strchr (mod,'\\'); p; p=strchr (p,'\\'))
		    *p='/';

		p = strrchr (mod,'/');
		if (p && (strchr (p,'?') || strchr (p,'*')))
		{
		    *p='\0';
		    regexp_match = p+1;
		}
		else
		    regexp_match = NULL;

		/* Frontends like to do 'ls -q /', so we support it explicitly.
                 */
		if (!strcmp (mod,"/"))
		{
		    *mod='\0';
		}

		err += do_module (db, mod, MISC, "Listing",
				  ls_proc, NULL, 0, 0, 0, 0, NULL);

		free (mod);
	    }
	}
	else
	{
	    /* should be ".", but do_recursion() 
	       fails this: assert ( strstr ( repository, "/./" ) == NULL ); */
	    char *topmod = xstrdup ("");
	    err += do_module (db, topmod, MISC, "Listing",
			      ls_proc, NULL, 0, 0, 0, 0, NULL);
	    free (topmod);
	}
	close_module (db);
    }
    else
	ls_proc (argc + 1, argv - 1, NULL, NULL, NULL, 0, 0, NULL, NULL);

    return err;
}



struct long_format_data
{
    char *header;
    char *time;
    char *footer;
};

static int
ls_print (Node *p, void *closure)
{
    if (long_format)
    {
	struct long_format_data *data = p->data;
	cvs_output_tagged ("text", data->header);
	if (data->time)
	    cvs_output_tagged ("date", data->time);
	if (data->footer)
	    cvs_output_tagged ("text", data->footer);
	cvs_output_tagged ("newline", NULL);
    }
    else
	cvs_output (p->data, 0);
    return 0;
}



static int
ls_print_dir (Node *p, void *closure)
{
    static bool printed = false;

    if (recurse && !(ls_prune_dirs && list_isempty (p->data)))
    {
        /* Keep track of whether we've printed.  If we have, then put a blank
         * line before directory headers, to separate the header from the
         * listing of the previous directory.
         */
        if (printed)
            cvs_output ("\n", 1);
        else
            printed = true;

        if (!strcmp (p->key, ""))
            cvs_output (".", 1);
        else
	    cvs_output (p->key, 0);
        cvs_output (":\n", 2);
    }
    walklist (p->data, ls_print, NULL);
    return 0;
}



/*
 * Delproc for a node containing a struct long_format_data as data.
 */
static void
long_format_data_delproc (Node *n)
{
	struct long_format_data *data = (struct long_format_data *)n->data;
	if (data->header) free (data->header);
	if (data->time) free (data->time);
	if (data->footer) free (data->footer);
	free (data);
}



/* A delproc for a List Node containing a List *.  */
static void
ls_delproc (Node *p)
{
    dellist ((List **)&p->data);
}



/*
 * Add a file to our list of data to print for a directory.
 */
/* ARGSUSED */
static int
ls_fileproc (void *callerdat, struct file_info *finfo)
{
    Vers_TS *vers;
    char *regex_err;
    Node *p, *n;
    bool isdead;
    const char *filename;

    if (regexp_match)
    {
#ifdef FILENAMES_CASE_INSENSITIVE
	  re_set_syntax (REG_ICASE|RE_SYNTAX_EGREP);
#else
	  re_set_syntax (RE_SYNTAX_EGREP);
#endif
	  if ((regex_err = re_comp (regexp_match)) != NULL)
	  {
	      error (1, 0, "bad regular expression passed to 'ls': %s",
                     regex_err);
	  }
	  if (re_exec (finfo->file) == 0)
	      return 0;				/* no match */
    }

    vers = Version_TS (finfo, NULL, show_tag, show_date, 1, 0);
    /* Skip dead revisions unless specifically requested to do otherwise.
     * We also bother to check for long_format so we can print the state.
     */
    if (vers->vn_rcs && (!show_dead_revs || long_format))
	isdead = RCS_isdead (finfo->rcs, vers->vn_rcs);
    else
	isdead = false;
    if (!vers->vn_rcs || (!show_dead_revs && isdead))
    {
        freevers_ts (&vers);
	return 0;
    }

    p = findnode (callerdat, finfo->update_dir);
    if (!p)
    {
	/* This only occurs when a complete path to a file is specified on the
	 * command line.  Put the file in the root list.
	 */
	filename = finfo->fullname;

	/* Add update_dir node.  */
	p = findnode (callerdat, ".");
	if (!p)
	{
	    p = getnode ();
	    p->key = xstrdup (".");
	    p->data = getlist ();
	    p->delproc = ls_delproc;
	    addnode (callerdat, p);
	}
    }
    else
	filename = finfo->file;

    n = getnode();
    if (entries_format)
    {
	char *outdate = entries_time (RCS_getrevtime (finfo->rcs, vers->vn_rcs,
                                                      0, 0));
	n->data = Xasprintf ("/%s/%s/%s/%s/%s%s\n",
                             filename, vers->vn_rcs,
                             outdate, vers->options,
                             show_tag ? "T" : "", show_tag ? show_tag : "");
	free (outdate);
    }
    else if (long_format)
    {
	struct long_format_data *out =
		xmalloc (sizeof (struct long_format_data));
	out->header = Xasprintf ("%-5.5s",
                                 vers->options[0] != '\0' ? vers->options
                                                          : "----");
	/* FIXME: Do we want to mimc the real `ls' command's date format?  */
	out->time = gmformat_time_t (RCS_getrevtime (finfo->rcs, vers->vn_rcs,
                                                     0, 0));
	out->footer = Xasprintf (" %-9.9s%s %s%s", vers->vn_rcs,
                                 strlen (vers->vn_rcs) > 9 ? "+" : " ",
                                 show_dead_revs ? (isdead ? "dead " : "     ")
                                                : "",
                                 filename);
	n->data = out;
	n->delproc = long_format_data_delproc;
    }
    else
	n->data = Xasprintf ("%s\n", filename);

    addnode (p->data, n);

    freevers_ts (&vers);
    return 0;
}



/*
 * Add this directory to the list of data to be printed for a directory and
 * decide whether to tell the recursion processor whether to continue
 * recursing or not.
 */
static Dtype
ls_direntproc (void *callerdat, const char *dir, const char *repos,
               const char *update_dir, List *entries)
{
    Dtype retval;
    Node *p;

    /* Due to the way we called start_recursion() from ls_proc() with a single
     * argument at a time, we can assume that if we don't yet have a parent
     * directory in DIRS then this directory should be processed.
     */

    if (strcmp (dir, "."))
    {
        /* Search for our parent directory.  */
	char *parent;
        parent = xmalloc (strlen (update_dir) - strlen (dir) + 1);
        strncpy (parent, update_dir, strlen (update_dir) - strlen (dir));
        parent[strlen (update_dir) - strlen (dir)] = '\0';
        strip_trailing_slashes (parent);
        p = findnode (callerdat, parent);
    }
    else
        p = NULL;

    if (p)
    {
	/* Push this dir onto our parent directory's listing.  */
	Node *n = getnode();

	if (entries_format)
	    n->data = Xasprintf ("D/%s////\n", dir);
	else if (long_format)
	{
	    struct long_format_data *out =
		    xmalloc (sizeof (struct long_format_data));
	    out->header = xstrdup ("d--- ");
	    out->time = gmformat_time_t (unix_time_stamp (repos));
	    out->footer = Xasprintf ("%12s%s%s", "",
                                     show_dead_revs ? "     " : "", dir);
	    n->data = out;
	    n->delproc = long_format_data_delproc;
	}
	else
	    n->data = Xasprintf ("%s\n", dir);

	addnode (p->data, n);
    }

    if (!p || recurse)
    {
	/* Create a new list for this directory.  */
	p = getnode ();
	p->key = xstrdup (strcmp (update_dir, ".") ? update_dir : "");
	p->data = getlist ();
        p->delproc = ls_delproc;
	addnode (callerdat, p);

	/* Create a local directory and mark it as needing deletion.  This is
         * the behavior the recursion processor relies upon, a la update &
         * checkout.
         */
	if (!isdir (dir))
        {
	    int nonbranch;
	    if (show_tag == NULL && show_date == NULL)
	    {
		ParseTag (&show_tag, &show_date, &nonbranch);
		set_tag = true;
	    }

	    if (!created_dir)
		created_dir = xstrdup (update_dir);

	    make_directory (dir);
	    Create_Admin (dir, update_dir, repos, show_tag, show_date,
			  nonbranch, 0, 0);
	    Subdir_Register (entries, NULL, dir);
	}

	/* Tell do_recursion to keep going.  */
	retval = R_PROCESS;
    }
    else
        retval = R_SKIP_ALL;

    return retval;
}



/* Clean up tags, dates, and dirs if we created this directory.
 */
static int
ls_dirleaveproc (void *callerdat, const char *dir, int err,
                 const char *update_dir, List *entries)
{
	if (created_dir && !strcmp (created_dir, update_dir))
	{
		if (set_tag)
		{
		    if (show_tag) free (show_tag);
		    if (show_date) free (show_date);
		    show_tag = show_date = NULL;
		    set_tag = false;
		}

		(void)CVS_CHDIR ("..");
		if (unlink_file_dir (dir))
		    error (0, errno, "Failed to remove directory `%s'",
			   created_dir);
		Subdir_Deregister (entries, NULL, dir);

		free (created_dir);
		created_dir = NULL;
	}
	return err;
}



static int
ls_proc (int argc, char **argv, char *xwhere, char *mwhere, char *mfile,
         int shorten, int local, char *mname, char *msg)
{
    char *repository;
    int err = 0;
    int which;
    char *where;
    int i;

    if (is_rls)
    {
	char *myargv[2];

	if (!quiet)
	    error (0, 0, "Listing module: `%s'",
	           strcmp (mname, "") ? mname : ".");

	repository = xmalloc (strlen (current_parsed_root->directory)
			      + strlen (argv[0])
			      + (mfile == NULL ? 0 : strlen (mfile) + 1)
			      + 2);
	(void)sprintf (repository, "%s/%s", current_parsed_root->directory,
		       argv[0]);
	where = xmalloc (strlen (argv[0])
			 + (mfile == NULL ? 0 : strlen (mfile) + 1)
			 + 1);
	(void)strcpy (where, argv[0]);

	/* If mfile isn't null, we need to set up to do only part of the
	 * module.
	 */
	if (mfile != NULL)
	{
	    char *cp;
	    char *path;

	    /* If the portion of the module is a path, put the dir part on
	     * repos.
	     */
	    if ((cp = strrchr (mfile, '/')) != NULL)
	    {
		*cp = '\0';
		(void)strcat (repository, "/");
		(void)strcat (repository, mfile);
		(void)strcat (where, "/");
		(void)strcat (where, mfile);
		mfile = cp + 1;
	    }

	    /* take care of the rest */
	    path = Xasprintf ("%s/%s", repository, mfile);
	    if (isdir (path))
	    {
		/* directory means repository gets the dir tacked on */
		(void)strcpy (repository, path);
		(void)strcat (where, "/");
		(void)strcat (where, mfile);
	    }
	    else
	    {
		myargv[1] = mfile;
		argc = 2;
		argv = myargv;
	    }
	    free (path);
	}

	/* cd to the starting repository */
	if (CVS_CHDIR (repository) < 0)
	{
	    error (0, errno, "cannot chdir to %s", repository);
	    free (repository);
	    free (where);
	    return 1;
	}

	which = W_REPOS;
    }
    else /* !is_rls */
    {
        repository = NULL;
        where = NULL;
        which = W_LOCAL | W_REPOS;
    }

    if (show_tag || show_date || show_dead_revs)
	which |= W_ATTIC;

    if (show_tag != NULL && !tag_validated)
    {
	tag_check_valid (show_tag, argc - 1, argv + 1, local, 0, repository,
			 false);
	tag_validated = true;
    }

    /* Loop on argc so that we are guaranteed that any directory passed to
     * ls_direntproc should be processed if its parent is not yet in DIRS.
     */
    if (argc == 1)
    {
	List *dirs = getlist ();
	err = start_recursion (ls_fileproc, NULL, ls_direntproc,
			       ls_dirleaveproc, dirs, 0, NULL, local, which, 0,
			       CVS_LOCK_READ, where, 1, repository);
	walklist (dirs, ls_print_dir, NULL);
	dellist (&dirs);
    }
    else
    {
	for (i = 1; i < argc; i++)
	{
	    List *dirs = getlist ();
	    err = start_recursion (ls_fileproc, NULL, ls_direntproc,
				   NULL, dirs, 1, argv + i, local, which, 0,
				   CVS_LOCK_READ, where, 1, repository);
	    walklist (dirs, ls_print_dir, NULL);
	    dellist (&dirs);
	}
    }

    if (!(which & W_LOCAL)) free (repository);
    if (where) free (where);

    return err;
}
