/* -*- C++ -*- code for handling directories (cd, pwd, ...)
   Copyright (C) 1992 Per Bothner.
   Partly based on Bash, Copyright (C) 1987-1991 Free Software Foundation, Inc.

This file is part of Q.

Q 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.

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

#include "gassign.h"
#include "shell.h"
#include "exceptions.h"
#include "genmap.h"
#include "shelldefs.h"
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#ifndef MAXPATHLEN
#define MAXPATHLEN 1024
#endif

static int change_to_directory (char *newdir);
extern "C" char *get_working_directory (char *for_whom);

/* Non-zero means that pwd always give verbatim directory, regardless of
   symbolic link following. */
static int verbatim_pwd = 1;

/* In order to keep track of the working directory, we have this static
   variable hanging around. */
static char *the_current_working_directory = (char *)NULL;
static char *old_current_working_directory = (char *)NULL;

class PWDvar : public Assignable {
    int old; // 0 for PWD; 1 for OLDPWD
    char **_ptr;
  public:
    PWDvar(int kind, char**ptr) : old(kind), _ptr(ptr) { }
    virtual void assign(Root *new_value);
    virtual Root *value();
};


PWDvar PWD(0, &the_current_working_directory);
PWDvar OLDPWD(1, &old_current_working_directory);

INSERT_BUILTIN(PWD, PWD);
INSERT_BUILTIN(OLDPWD, OLDPWD);

void PWDvar::assign(Root *new_value)
{
}

Root* PWDvar::value()
{
    char dirbuf[MAXPATHLEN];
    char *dir;
    if (*_ptr)
	dir = *_ptr;
    else if (old == 1) {
	Signal(new UnboundVariable(NULL));
	return NULL; // ???
    }
    else {
	if (getcwd (dirbuf, sizeof (dirbuf)) == NULL)
	    Signal(new BadSyscall("System call pwd failed", NULL, errno));
	dir = dirbuf;
    }
    return NewString(strlen(dir), dir);
}

Root * DoPwd()
{
    char dirbuf[MAXPATHLEN];
    if (getcwd(dirbuf, sizeof(dirbuf)) == NULL)
	Signal(new BadSyscall("System call pwd failed", NULL, errno));
    int len = strlen(dirbuf);
    StringC* str = NewString(len+1, dirbuf);
    str->chars()[len] = '\n';
    return str;
}

/* Return 1 if STRING contains an absolute pathname, else 0. */
int absolute_pathname (char *string)
{
  if (!string || !*string)
    return (0);

  if (*string == '/')
    return (1);

#if 0
  if (*string++ == '.')
    {
      if ((!*string) || *string == '/')
	return (1);

      if (*string++ == '.')
	if (!*string || *string == '/')
	  return (1);
    }
#endif
  return (0);
}

Root* CdAction(Root* args)
{
    StringList* strings = GlobList(args);
    int argc = StringsCount(strings);
    char *dirname;

#if 0
  {
    extern int restricted;
    if (restricted)
      {
	builtin_error ("Privileged command");
	return (EXECUTION_FAILURE);
      }
  }
#endif

  if (argc > 0)
    {
      char *path_string = get_string_value ("CDPATH");
      char *path;
      int index = 0;

      StringC *string = StringsPtr(strings)[0];
      dirname = string->chars();

      if (path_string && !absolute_pathname (dirname))
	{
	  while ((path = extract_colon_unit (path_string, &index)))
	    {
	      char *dir;

	      if (*path && (*path == '~'))
		{
		  char *te_string = tilde_expand (path);

		  free (path);
		  path = te_string;
		}

	      if (!*path)
		{
		  free (path);
		  path = savestring ("."); /* by definition. */
		}

	      dir = (char *)alloca (2 + strlen (dirname) + strlen (path));

	      if (!dir)
		abort ();

	      strcpy (dir, path);
	      if (path[strlen (path) - 1] != '/')
		strcat (dir, "/");
	      strcat (dir, dirname);
	      free (path);

	      if (change_to_directory (dir))
		{
		  if (strncmp (dir, "./", 2) != 0)
		    printf ("%s\n", dir);
		  dirname = dir;

		  goto bind_and_exit;
		}
	    }
	}

      if (!change_to_directory (dirname))
	{
	  /* Maybe this is `cd -', equivalent to `cd $OLDPWD' */
	  if (dirname[0] == '-' && dirname[1] == '\0')
	    {
	      char *t = old_current_working_directory;

	      if (t && change_to_directory (t))
		goto bind_and_exit;
	    }

#if 0
	  /* If the user requests it, then perhaps this is the name of
	     a shell variable, whose value contains the directory to
	     change to.  If that is the case, then change to that
	     directory. */
	  if (find_variable ("cdable_vars"))
	    {
	      char *t = get_string_value (dirname);

	      if (t && change_to_directory (t))
		{
		  printf ("%s\n", t);
		  goto bind_and_exit;
		}
	    }
#endif

	  Signal(new BadSyscall("cd \"%s\" failed",
				strdup(dirname), errno));
	}
      goto bind_and_exit;
    }
  else
    {
      dirname = get_string_value ("HOME");

      if (!dirname)
	  Signal(new BadSyscall("cd failed - no value for HOME", "", 0));

      if (!change_to_directory (dirname))
	  Signal(new BadSyscall("cd to HOME (%s) failed",
				strdup(dirname), errno));

    bind_and_exit:
      return &NullSequence;
    }
}

/* Remove the last N directories from PATH.  Do not PATH blank.
   PATH must contain enoung space for MAXPATHLEN characters. */
void pathname_backup (char *path, int n)
{
  register char *p;

  if (!*path)
    return;

  p = path + (strlen (path) - 1);

  while (n--)
    {
      while (*p == '/' && p != path)
	p--;

      while (*p != '/' && p != path)
	p--;

      *++p = '\0';
    }
}

/* Turn STRING (a pathname) into an absolute pathname, assuming that
   DOT_PATH contains the symbolic location of '.'.  This always
   returns a new string, even if STRING was an absolute pathname to
   begin with. */

static char current_path[MAXPATHLEN];

char *make_absolute (char *string, char *dot_path)
{
  register char *cp;

  if (!dot_path || *string == '/')
    return (savestring (string));

  strcpy (current_path, dot_path);

  if (!current_path[0])
    strcpy (current_path, "./");

  cp = current_path + (strlen (current_path) - 1);

  if (*cp++ != '/')
    *cp++ = '/';

  *cp = '\0';

  while (*string)
    {
      if (*string == '.')
	{
	  if (!string[1])
	    return (savestring (current_path));

	  if (string[1] == '/')
	    {
	      string += 2;
	      continue;
	    }

	  if (string[1] == '.' && (string[2] == '/' || !string[2]))
	    {
	      string += 2;

	      if (*string)
		string++;

	      pathname_backup (current_path, 1);
	      cp = current_path + strlen (current_path);
	      continue;
	    }
	}

      while (*string && *string != '/')
	*cp++ = *string++;

      if (*string)
	*cp++ = *string++;

      *cp = '\0';
    }
  return (savestring (current_path));
}

/* Change the current working directory to the first word in LIST, or
   to $HOME if there is no LIST.  Do nothing in the case that there is
   no $HOME nor LIST. If the variable CDPATH exists, use that as the search
   path for finding the directory.  If all that fails, and the variable
   `cdable_vars' exists, then try the word as a variable name.  If that
   variable has a value, then cd to the value of that variable. */

/* By default, follow the symbolic links as if they were real directories
   while hacking the `cd' command.  This means that `cd ..' moves up in
   the string of symbolic links that make up the current directory, instead
   of the absolute directory.  The shell variable `nolinks' controls this
   flag. */
int follow_symbolic_links = 1;

/* Return a consed string which is the current working directory.
   FOR_WHOM is the name of the caller for error printing.  */
extern "C" char *get_working_directory (char *for_whom)
{
  if (!follow_symbolic_links)
    {
      if (the_current_working_directory)
	free (the_current_working_directory);

      the_current_working_directory = (char *)NULL;
    }

  if (!the_current_working_directory)
    {
      char *directory, *getwd ();

      the_current_working_directory = (char *)xmalloc (MAXPATHLEN);
      directory = getwd (the_current_working_directory);
      if (!directory)
	{
	  fprintf (stderr, "%s: %s\n",
		   for_whom, the_current_working_directory);
	  free (the_current_working_directory);
	  the_current_working_directory = (char *)NULL;
	  return (char *)NULL;
	}
    }

  return (savestring (the_current_working_directory));
}

void update_pwd(char *new_directory)
{
  if (old_current_working_directory)
      free (old_current_working_directory);
  old_current_working_directory = the_current_working_directory;
  the_current_working_directory = new_directory;
}

/* Do the work of changing to the directory NEWDIR.  Handle symbolic
   link following, etc. */
static int change_to_directory (char *newdir)
{
  char *new_directory;

  if (follow_symbolic_links)
    {
      if (!the_current_working_directory)
	{
	  new_directory = get_working_directory ("cd_links");
	  if (new_directory)
	    free (new_directory);
	}

      if (the_current_working_directory)
	new_directory = make_absolute (newdir, the_current_working_directory);
      else
	new_directory = savestring (newdir);

      /* Get rid of trailing `/'. */
      {
	register int len_t = strlen (new_directory);
	if (len_t > 1)
	  {
	    --len_t;
	    if (new_directory[len_t] == '/')
	      new_directory[len_t] = '\0';
	  }
      }

      if (chdir (new_directory) < 0)
	{
	  free (new_directory);
	  return (0);
	}

    }
  else
    {
      if (chdir (newdir) < 0)
	return (0);
      new_directory = get_working_directory ("cd");
    }
  update_pwd(new_directory);
  return 1;
}

char* my_tilde_expand(char* text)
{
    char *result = NULL;
    if (strcmp (text, "-") == 0)
	result = old_current_working_directory;
    else if (strcmp (text, "+") == 0)
	result = the_current_working_directory;

    if (result)
	result = strdup (result);

    return result;
}
