/*
   cd_to() function.

   Copyright (C) 1995-2024
   Free Software Foundation, Inc.

   Written by:
   Slava Zanko <slavazanko@gmail.com>, 2013
   Andrew Borodin <aborodin@vmail.ru>, 2020

   This file is part of the Midnight Commander.

   The Midnight Commander 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 3 of the License,
   or (at your option) any later version.

   The Midnight Commander 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, see <http://www.gnu.org/licenses/>.
 */

/** \file cd.c
 *  \brief Source: cd_to() function
 */

#include <config.h>

#include <stdlib.h>
#include <errno.h>
#include <string.h>

#include "lib/global.h"
#include "lib/vfs/vfs.h"
#include "lib/strescape.h"      /* strutils_shell_unescape() */
#include "lib/util.h"           /* whitespace() */
#include "lib/widget.h"         /* message() */

#include "filemanager.h"        /* current_panel, panel.h, layout.h */
#include "tree.h"               /* sync_tree() */

#include "cd.h"

/*** global variables ****************************************************************************/

/*** file scope macro definitions ****************************************************************/

/*** file scope type declarations ****************************************************************/

/*** forward declarations (file scope functions) *************************************************/

/*** file scope variables ************************************************************************/

/* --------------------------------------------------------------------------------------------- */
/*** file scope functions ************************************************************************/
/* --------------------------------------------------------------------------------------------- */

/**
 * Expand the argument to "cd" and change directory.  First try tilde
 * expansion, then variable substitution.  If the CDPATH variable is set
 * (e.g. CDPATH=".:~:/usr"), try all the paths contained there.
 * We do not support such rare substitutions as ${var:-value} etc.
 * No quoting is implemented here, so ${VAR} and $VAR will be always
 * substituted.  Wildcards are not supported either.
 * Advanced users should be encouraged to use "\cd" instead of "cd" if
 * they want the behavior they are used to in the shell.
 *
 * @param _path string to examine
 * @return newly allocated string
 */

static GString *
examine_cd (const char *_path)
{
    /* *INDENT-OFF* */
    typedef enum
    {
        copy_sym,
        subst_var
    } state_t;
    /* *INDENT-ON* */

    state_t state = copy_sym;
    GString *q;
    char *path_tilde, *path;
    char *p;

    /* Tilde expansion */
    path = strutils_shell_unescape (_path);
    path_tilde = tilde_expand (path);
    g_free (path);

    q = g_string_sized_new (32);

    /* Variable expansion */
    for (p = path_tilde; *p != '\0';)
    {
        switch (state)
        {
        case copy_sym:
            if (p[0] == '\\' && p[1] == '$')
            {
                g_string_append_c (q, '$');
                p += 2;
            }
            else if (p[0] != '$' || p[1] == '[' || p[1] == '(')
            {
                g_string_append_c (q, *p);
                p++;
            }
            else
                state = subst_var;
            break;

        case subst_var:
            {
                char *s = NULL;
                char c;
                const char *t = NULL;

                /* skip dollar */
                p++;

                if (p[0] == '{')
                {
                    p++;
                    s = strchr (p, '}');
                }
                if (s == NULL)
                    s = strchr (p, PATH_SEP);
                if (s == NULL)
                    s = strchr (p, '\0');
                c = *s;
                *s = '\0';
                t = getenv (p);
                *s = c;
                if (t == NULL)
                {
                    g_string_append_c (q, '$');
                    if (p[-1] != '$')
                        g_string_append_c (q, '{');
                }
                else
                {
                    g_string_append (q, t);
                    p = s;
                    if (*s == '}')
                        p++;
                }

                state = copy_sym;
                break;
            }

        default:
            break;
        }
    }

    g_free (path_tilde);

    return q;
}

/* --------------------------------------------------------------------------------------------- */

/* CDPATH handling */
static gboolean
handle_cdpath (const char *path)
{
    gboolean result = FALSE;

    /* CDPATH handling */
    if (!IS_PATH_SEP (*path))
    {
        char *cdpath, *p;
        char c;

        cdpath = g_strdup (getenv ("CDPATH"));
        p = cdpath;
        c = (p == NULL) ? '\0' : ':';

        while (!result && c == ':')
        {
            char *s;

            s = strchr (p, ':');
            if (s == NULL)
                s = strchr (p, '\0');
            c = *s;
            *s = '\0';
            if (*p != '\0')
            {
                vfs_path_t *r_vpath;

                r_vpath = vfs_path_build_filename (p, path, (char *) NULL);
                result = panel_cd (current_panel, r_vpath, cd_parse_command);
                vfs_path_free (r_vpath, TRUE);
            }
            *s = c;
            p = s + 1;
        }
        g_free (cdpath);
    }

    return result;
}

/* --------------------------------------------------------------------------------------------- */
/*** public functions ****************************************************************************/
/* --------------------------------------------------------------------------------------------- */

/** Execute the cd command to specified path
 *
 * @param path path to cd
 */

void
cd_to (const char *path)
{
    char *p;

    /* Remove leading whitespaces. */
    /* Any final whitespace should be removed here (to see why, try "cd fred "). */
    /* NOTE: I think we should not remove the extra space,
       that way, we can cd into hidden directories */
    /* FIXME: what about interpreting quoted strings like the shell.
       so one could type "cd <tab> M-a <enter>" and it would work. */
    p = g_strstrip (g_strdup (path));

    if (get_current_type () == view_tree)
    {
        vfs_path_t *new_vpath = NULL;

        if (p[0] == '\0')
        {
            new_vpath = vfs_path_from_str (mc_config_get_home_dir ());
            sync_tree (new_vpath);
        }
        else if (DIR_IS_DOTDOT (p))
        {
            if (vfs_path_elements_count (current_panel->cwd_vpath) != 1 ||
                strlen (vfs_path_get_by_index (current_panel->cwd_vpath, 0)->path) > 1)
            {
                vfs_path_t *tmp_vpath = current_panel->cwd_vpath;

                current_panel->cwd_vpath =
                    vfs_path_vtokens_get (tmp_vpath, 0, vfs_path_tokens_count (tmp_vpath) - 1);
                vfs_path_free (tmp_vpath, TRUE);
            }
            sync_tree (current_panel->cwd_vpath);
        }
        else
        {
            if (IS_PATH_SEP (*p))
                new_vpath = vfs_path_from_str (p);
            else
                new_vpath = vfs_path_append_new (current_panel->cwd_vpath, p, (char *) NULL);

            sync_tree (new_vpath);
        }

        vfs_path_free (new_vpath, TRUE);
    }
    else
    {
        GString *s_path;
        vfs_path_t *q_vpath;
        gboolean ok;

        s_path = examine_cd (p);

        if (s_path->len == 0)
            q_vpath = vfs_path_from_str (mc_config_get_home_dir ());
        else
            q_vpath = vfs_path_from_str_flags (s_path->str, VPF_NO_CANON);

        ok = panel_cd (current_panel, q_vpath, cd_parse_command);
        if (!ok)
            ok = handle_cdpath (s_path->str);
        if (!ok)
        {
            char *d;

            d = vfs_path_to_str_flags (q_vpath, 0, VPF_STRIP_PASSWORD);
            cd_error_message (d);
            g_free (d);
        }

        vfs_path_free (q_vpath, TRUE);
        g_string_free (s_path, TRUE);
    }

    g_free (p);
}

/* --------------------------------------------------------------------------------------------- */

void
cd_error_message (const char *path)
{
    message (D_ERROR, MSG_ERROR, _("Cannot change directory to\n%s\n%s"), path,
             unix_error_string (errno));
}

/* --------------------------------------------------------------------------------------------- */
