/*
   Tree Store
   Contains a storage of the file system tree representation

   This module has been converted to be a widget.

   The program load and saves the tree each time the tree widget is
   created and destroyed.  This is required for the future vfs layer,
   it will be possible to have tree views over virtual file systems.

   Copyright (C) 1999-2025
   Free Software Foundation, Inc.

   Written by:
   Janne Kukonlehto, 1994, 1996
   Norbert Warmuth, 1997
   Miguel de Icaza, 1996, 1999
   Slava Zanko <slavazanko@gmail.com>, 2013
   Andrew Borodin <aborodin@vmail.ru>, 2013

   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 treestore.c
 *  \brief Source: tree store
 *
 *  Contains a storage of the file system tree representation.
 */

#include <config.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include "lib/global.h"
#include "lib/mcconfig.h"
#include "lib/vfs/vfs.h"
#include "lib/fileloc.h"
#include "lib/strutil.h"
#include "lib/hook.h"
#include "lib/util.h"

#include "src/setup.h"          /* setup_init() */

#include "treestore.h"

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

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

#define TREE_SIGNATURE "Midnight Commander TreeStore v 2.0"

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

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

static tree_entry *tree_store_add_entry (const vfs_path_t * name);

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

static struct TreeStore ts;

static hook_t *remove_entry_hooks;

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

static inline void
tree_store_dirty (gboolean dirty)
{
    ts.dirty = dirty;
}

/* --------------------------------------------------------------------------------------------- */
/**
  *
  * @return the number of common bytes in the strings.
  */

static size_t
str_common (const vfs_path_t *s1_vpath, const vfs_path_t *s2_vpath)
{
    size_t result = 0;
    const char *s1, *s2;

    s1 = vfs_path_as_str (s1_vpath);
    s2 = vfs_path_as_str (s2_vpath);

    while (*s1 != '\0' && *s2 != '\0' && *s1++ == *s2++)
        result++;

    return result;
}

/* --------------------------------------------------------------------------------------------- */
/** The directory names are arranged in a single linked list in the same
  * order as they are displayed. When the tree is displayed the expected
  * order is like this:
  * /
  * /bin
  * /etc
  * /etc/X11
  * /etc/rc.d
  * /etc.old/X11
  * /etc.old/rc.d
  * /usr
  *
  * i.e. the required collating sequence when comparing two directory names is 
  * '\0' < PATH_SEP < all-other-characters-in-encoding-order
  *
  * Since strcmp doesn't fulfil this requirement we use pathcmp when
  * inserting directory names into the list. The meaning of the return value 
  * of pathcmp and strcmp are the same (an integer less than, equal to, or 
  * greater than zero if p1 is found to be less than, to match, or be greater 
  * than p2.
 */

static int
pathcmp (const vfs_path_t *p1_vpath, const vfs_path_t *p2_vpath)
{
    int ret_val;
    const char *p1, *p2;

    p1 = vfs_path_as_str (p1_vpath);
    p2 = vfs_path_as_str (p2_vpath);

    for (; *p1 == *p2; p1++, p2++)
        if (*p1 == '\0')
            return 0;

    if (*p1 == '\0')
        ret_val = -1;
    else if (*p2 == '\0')
        ret_val = 1;
    else if (IS_PATH_SEP (*p1))
        ret_val = -1;
    else if (IS_PATH_SEP (*p2))
        ret_val = 1;
    else
        ret_val = (*p1 - *p2);

    return ret_val;
}

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

static char *
decode (char *buffer)
{
    char *res, *p, *q;

    res = g_strdup (buffer);

    for (p = q = res; *p != '\0'; p++, q++)
    {
        if (*p == '\n')
        {
            *q = '\0';
            return res;
        }

        if (*p != '\\')
        {
            *q = *p;
            continue;
        }

        p++;

        switch (*p)
        {
        case 'n':
            *q = '\n';
            break;
        case '\\':
            *q = '\\';
            break;
        default:
            break;
        }
    }

    *q = *p;

    return res;
}

/* --------------------------------------------------------------------------------------------- */
/** Loads the tree store from the specified filename */

static int
tree_store_load_from (const char *name)
{
    FILE *file;
    char buffer[MC_MAXPATHLEN + 20];

    g_return_val_if_fail (name != NULL, 0);

    if (ts.loaded)
        return 1;

    file = fopen (name, "r");

    if (file != NULL
        && (fgets (buffer, sizeof (buffer), file) == NULL
            || strncmp (buffer, TREE_SIGNATURE, strlen (TREE_SIGNATURE)) != 0))
    {
        fclose (file);
        file = NULL;
    }

    if (file != NULL)
    {
        char oldname[MC_MAXPATHLEN] = "\0";

        ts.loaded = TRUE;

        /* File open -> read contents */
        while (fgets (buffer, MC_MAXPATHLEN, file))
        {
            tree_entry *e;
            gboolean scanned;
            char *lc_name;

            /* Skip invalid records */
            if (buffer[0] != '0' && buffer[0] != '1')
                continue;

            if (buffer[1] != ':')
                continue;

            scanned = buffer[0] == '1';

            lc_name = decode (buffer + 2);
            if (!IS_PATH_SEP (lc_name[0]))
            {
                /* Clear-text decompression */
                char *s;

                s = strtok (lc_name, " ");
                if (s != NULL)
                {
                    char *different;
                    int common;

                    common = atoi (s);
                    different = strtok (NULL, "");
                    if (different != NULL)
                    {
                        vfs_path_t *vpath;

                        vpath = vfs_path_from_str (oldname);
                        g_strlcpy (oldname + common, different, sizeof (oldname) - (size_t) common);
                        if (vfs_file_is_local (vpath))
                        {
                            vfs_path_t *tmp_vpath;

                            tmp_vpath = vfs_path_from_str (oldname);
                            e = tree_store_add_entry (tmp_vpath);
                            vfs_path_free (tmp_vpath, TRUE);
                            e->scanned = scanned;
                        }
                        vfs_path_free (vpath, TRUE);
                    }
                }
            }
            else
            {
                vfs_path_t *vpath;

                vpath = vfs_path_from_str (lc_name);
                if (vfs_file_is_local (vpath))
                {
                    e = tree_store_add_entry (vpath);
                    e->scanned = scanned;
                }
                vfs_path_free (vpath, TRUE);
                g_strlcpy (oldname, lc_name, sizeof (oldname));
            }
            g_free (lc_name);
        }

        fclose (file);
    }

    /* Nothing loaded, we add some standard directories */
    if (!ts.tree_first)
    {
        vfs_path_t *tmp_vpath;

        tmp_vpath = vfs_path_from_str (PATH_SEP_STR);
        tree_store_add_entry (tmp_vpath);
        tree_store_rescan (tmp_vpath);
        vfs_path_free (tmp_vpath, TRUE);
        ts.loaded = TRUE;
    }

    return 1;
}

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

static char *
encode (const vfs_path_t *vpath, size_t offset)
{
    return str_escape (vfs_path_as_str (vpath) + offset, -1, "\n\\", FALSE);
}

/* --------------------------------------------------------------------------------------------- */
/** Saves the tree to the specified filename */

static int
tree_store_save_to (char *name)
{
    tree_entry *current;
    FILE *file;

    file = fopen (name, "w");
    if (file == NULL)
        return errno;

    fprintf (file, "%s\n", TREE_SIGNATURE);

    for (current = ts.tree_first; current != NULL; current = current->next)
        if (vfs_file_is_local (current->name))
        {
            int i, common;

            /* Clear-text compression */
            if (current->prev != NULL
                && (common = str_common (current->prev->name, current->name)) > 2)
            {
                char *encoded;

                encoded = encode (current->name, common);
                i = fprintf (file, "%d:%d %s\n", current->scanned ? 1 : 0, common, encoded);
                g_free (encoded);
            }
            else
            {
                char *encoded;

                encoded = encode (current->name, 0);
                i = fprintf (file, "%d:%s\n", current->scanned ? 1 : 0, encoded);
                g_free (encoded);
            }

            if (i == EOF)
            {
                fprintf (stderr, _("Cannot write to the %s file:\n%s\n"),
                         name, unix_error_string (errno));
                break;
            }
        }

    tree_store_dirty (FALSE);
    fclose (file);

    return 0;
}

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

static tree_entry *
tree_store_add_entry (const vfs_path_t *name)
{
    int flag = -1;
    tree_entry *current;
    tree_entry *old = NULL;
    tree_entry *new;
    int submask = 0;

    if (ts.tree_last != NULL && ts.tree_last->next != NULL)
        abort ();

    /* Search for the correct place */
    for (current = ts.tree_first;
         current != NULL && (flag = pathcmp (current->name, name)) < 0; current = current->next)
        old = current;

    if (flag == 0)
        return current;         /* Already in the list */

    /* Not in the list -> add it */
    new = g_new0 (tree_entry, 1);
    if (current == NULL)
    {
        /* Append to the end of the list */
        if (ts.tree_first == NULL)
        {
            /* Empty list */
            ts.tree_first = new;
            new->prev = NULL;
        }
        else
        {
            if (old != NULL)
                old->next = new;
            new->prev = old;
        }
        new->next = NULL;
        ts.tree_last = new;
    }
    else
    {
        /* Insert in to the middle of the list */
        new->prev = old;
        if (old != NULL)
        {
            /* Yes, in the middle */
            new->next = old->next;
            old->next = new;
        }
        else
        {
            /* Nope, in the beginning of the list */
            new->next = ts.tree_first;
            ts.tree_first = new;
        }
        new->next->prev = new;
    }

    /* Calculate attributes */
    new->name = vfs_path_clone (name);
    new->sublevel = vfs_path_tokens_count (new->name);

    {
        const char *new_name;

        new_name = vfs_path_get_last_path_str (new->name);
        new->subname = strrchr (new_name, PATH_SEP);
        if (new->subname == NULL)
            new->subname = new_name;
        else
            new->subname++;
    }

    if (new->next != NULL)
        submask = new->next->submask;

    submask |= 1 << new->sublevel;
    submask &= (2 << new->sublevel) - 1;
    new->submask = submask;
    new->mark = FALSE;

    /* Correct the submasks of the previous entries */
    for (current = new->prev;
         current != NULL && current->sublevel > new->sublevel; current = current->prev)
        current->submask |= 1 << new->sublevel;

    tree_store_dirty (TRUE);
    return new;
}

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

static void
tree_store_notify_remove (tree_entry *entry)
{
    hook_t *p;

    for (p = remove_entry_hooks; p != NULL; p = p->next)
    {
        tree_store_remove_fn r = (tree_store_remove_fn) p->hook_fn;

        r (entry, p->hook_data);
    }
}

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

static tree_entry *
remove_entry (tree_entry *entry)
{
    tree_entry *current = entry->prev;
    long submask = 0;
    tree_entry *ret = NULL;

    tree_store_notify_remove (entry);

    /* Correct the submasks of the previous entries */
    if (entry->next != NULL)
        submask = entry->next->submask;

    for (; current != NULL && current->sublevel > entry->sublevel; current = current->prev)
    {
        submask |= 1 << current->sublevel;
        submask &= (2 << current->sublevel) - 1;
        current->submask = submask;
    }

    /* Unlink the entry from the list */
    if (entry->prev != NULL)
        entry->prev->next = entry->next;
    else
        ts.tree_first = entry->next;

    if (entry->next != NULL)
        entry->next->prev = entry->prev;
    else
        ts.tree_last = entry->prev;

    /* Free the memory used by the entry */
    vfs_path_free (entry->name, TRUE);
    g_free (entry);

    return ret;
}

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

static void
process_special_dirs (GList **special_dirs, const char *file)
{
    gchar **start_buff;
    mc_config_t *cfg;

    cfg = mc_config_init (file, TRUE);
    if (cfg == NULL)
        return;

    start_buff = mc_config_get_string_list (cfg, "Special dirs", "list", NULL);
    if (start_buff != NULL)
    {
        gchar **buffers;

        for (buffers = start_buff; *buffers != NULL; buffers++)
        {
            *special_dirs = g_list_prepend (*special_dirs, *buffers);
            *buffers = NULL;
        }

        g_strfreev (start_buff);
    }
    mc_config_deinit (cfg);
}

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

static gboolean
should_skip_directory (const vfs_path_t *vpath)
{
    static GList *special_dirs = NULL;
    GList *l;
    static gboolean loaded = FALSE;
    gboolean ret = FALSE;

    if (!loaded)
    {
        const char *profile_name;

        profile_name = setup_init ();
        process_special_dirs (&special_dirs, profile_name);
        process_special_dirs (&special_dirs, mc_global.profile_name);

        loaded = TRUE;
    }

    for (l = special_dirs; l != NULL; l = g_list_next (l))
        if (strncmp (vfs_path_as_str (vpath), l->data, strlen (l->data)) == 0)
        {
            ret = TRUE;
            break;
        }

    return ret;
}

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

static void
queue_vpath_free (gpointer data)
{
    vfs_path_free ((vfs_path_t *) data, TRUE);
}

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

/* Searches for specified directory */
tree_entry *
tree_store_whereis (const vfs_path_t *name)
{
    tree_entry *current;
    int flag = -1;

    for (current = ts.tree_first;
         current != NULL && (flag = pathcmp (current->name, name)) < 0; current = current->next)
        ;

    return flag == 0 ? current : NULL;
}

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

struct TreeStore *
tree_store_get (void)
{
    return &ts;
}

/* --------------------------------------------------------------------------------------------- */
/**
 * \fn int tree_store_load(void)
 * \brief Loads the tree from the default location
 * \return 1 if success (true), 0 otherwise (false)
 */

int
tree_store_load (void)
{
    char *name;
    int retval;

    name = mc_config_get_full_path (MC_TREESTORE_FILE);
    retval = tree_store_load_from (name);
    g_free (name);

    return retval;
}

/* --------------------------------------------------------------------------------------------- */
/**
 * \fn int tree_store_save(void)
 * \brief Saves the tree to the default file in an atomic fashion
 * \return 0 if success, errno on error
 */

int
tree_store_save (void)
{
    char *name;
    int retval;

    name = mc_config_get_full_path (MC_TREESTORE_FILE);
    mc_util_make_backup_if_possible (name, ".tmp");

    retval = tree_store_save_to (name);
    if (retval != 0)
        mc_util_restore_from_backup_if_possible (name, ".tmp");
    else
        mc_util_unlink_backup_if_possible (name, ".tmp");

    g_free (name);
    return retval;
}

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

void
tree_store_add_entry_remove_hook (tree_store_remove_fn callback, void *data)
{
    add_hook (&remove_entry_hooks, (void (*)(void *)) callback, data);
}

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

void
tree_store_remove_entry_remove_hook (tree_store_remove_fn callback)
{
    delete_hook (&remove_entry_hooks, (void (*)(void *)) callback);
}

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

void
tree_store_remove_entry (const vfs_path_t *name_vpath)
{
    tree_entry *current, *base;
    size_t len;

    g_return_if_fail (name_vpath != NULL);

    /* Miguel Ugly hack */
    {
        gboolean is_root;
        const char *name_vpath_str;

        name_vpath_str = vfs_path_as_str (name_vpath);
        is_root = (IS_PATH_SEP (name_vpath_str[0]) && name_vpath_str[1] == '\0');
        if (is_root)
            return;
    }
    /* Miguel Ugly hack end */

    base = tree_store_whereis (name_vpath);
    if (base == NULL)
        return;                 /* Doesn't exist */

    len = vfs_path_len (base->name);
    current = base->next;
    while (current != NULL && vfs_path_equal_len (current->name, base->name, len))
    {
        gboolean ok;
        tree_entry *old;
        const char *cname;

        cname = vfs_path_as_str (current->name);
        ok = (cname[len] == '\0' || IS_PATH_SEP (cname[len]));
        if (!ok)
            break;

        old = current;
        current = current->next;
        remove_entry (old);
    }
    remove_entry (base);
    tree_store_dirty (TRUE);
}

/* --------------------------------------------------------------------------------------------- */
/** This subdirectory exists -> clear deletion mark */

void
tree_store_mark_checked (const char *subname)
{
    vfs_path_t *name;
    tree_entry *current, *base;
    int flag = 1;
    const char *cname;

    if (!ts.loaded)
        return;

    if (ts.check_name == NULL)
        return;

    /* Calculate the full name of the subdirectory */
    if (DIR_IS_DOT (subname) || DIR_IS_DOTDOT (subname))
        return;

    cname = vfs_path_as_str (ts.check_name);
    if (IS_PATH_SEP (cname[0]) && cname[1] == '\0')
        name = vfs_path_build_filename (PATH_SEP_STR, subname, (char *) NULL);
    else
        name = vfs_path_append_new (ts.check_name, subname, (char *) NULL);

    /* Search for the subdirectory */
    for (current = ts.check_start;
         current != NULL && (flag = pathcmp (current->name, name)) < 0; current = current->next)
        ;

    if (flag != 0)
    {
        /* Doesn't exist -> add it */
        current = tree_store_add_entry (name);
        ts.add_queue_vpath = g_list_prepend (ts.add_queue_vpath, name);
    }
    else
        vfs_path_free (name, TRUE);

    /* Clear the deletion mark from the subdirectory and its children */
    base = current;
    if (base != NULL)
    {
        size_t len;

        len = vfs_path_len (base->name);
        base->mark = FALSE;
        for (current = base->next;
             current != NULL && vfs_path_equal_len (current->name, base->name, len);
             current = current->next)
        {
            gboolean ok;

            cname = vfs_path_as_str (current->name);
            ok = (cname[len] == '\0' || IS_PATH_SEP (cname[len]) || len == 1);
            if (!ok)
                break;

            current->mark = FALSE;
        }
    }
}

/* --------------------------------------------------------------------------------------------- */
/** Mark the subdirectories of the current directory for delete */

tree_entry *
tree_store_start_check (const vfs_path_t *vpath)
{
    tree_entry *current, *retval;
    size_t len;

    if (!ts.loaded)
        return NULL;

    g_return_val_if_fail (ts.check_name == NULL, NULL);
    ts.check_start = NULL;

    /* Search for the start of subdirectories */
    current = tree_store_whereis (vpath);
    if (current == NULL)
    {
        mc_stat_t s;

        if (mc_stat (vpath, &s) == -1 || !S_ISDIR (s.st_mode))
            return NULL;

        current = tree_store_add_entry (vpath);
        ts.check_name = vfs_path_clone (vpath);

        return current;
    }

    ts.check_name = vfs_path_clone (vpath);

    retval = current;

    /* Mark old subdirectories for delete */
    ts.check_start = current->next;
    len = vfs_path_len (ts.check_name);

    for (current = ts.check_start;
         current != NULL && vfs_path_equal_len (current->name, ts.check_name, len);
         current = current->next)
    {
        gboolean ok;
        const char *cname;

        cname = vfs_path_as_str (current->name);
        ok = (cname[len] == '\0' || IS_PATH_SEP (cname[len]) || len == 1);
        if (!ok)
            break;

        current->mark = TRUE;
    }

    return retval;
}

/* --------------------------------------------------------------------------------------------- */
/** Delete subdirectories which still have the deletion mark */

void
tree_store_end_check (void)
{
    tree_entry *current;
    size_t len;
    GList *the_queue;

    if (!ts.loaded)
        return;

    g_return_if_fail (ts.check_name != NULL);

    /* Check delete marks and delete if found */
    len = vfs_path_len (ts.check_name);

    current = ts.check_start;
    while (current != NULL && vfs_path_equal_len (current->name, ts.check_name, len))
    {
        gboolean ok;
        tree_entry *old;
        const char *cname;

        cname = vfs_path_as_str (current->name);
        ok = (cname[len] == '\0' || IS_PATH_SEP (cname[len]) || len == 1);
        if (!ok)
            break;

        old = current;
        current = current->next;
        if (old->mark)
            remove_entry (old);
    }

    /* get the stuff in the scan order */
    the_queue = g_list_reverse (ts.add_queue_vpath);
    ts.add_queue_vpath = NULL;
    vfs_path_free (ts.check_name, TRUE);
    ts.check_name = NULL;

    g_list_free_full (the_queue, queue_vpath_free);
}

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

tree_entry *
tree_store_rescan (const vfs_path_t *vpath)
{
    DIR *dirp;
    mc_stat_t buf;
    tree_entry *entry;

    if (should_skip_directory (vpath))
    {
        entry = tree_store_add_entry (vpath);
        entry->scanned = TRUE;
        return entry;
    }

    entry = tree_store_start_check (vpath);
    if (entry == NULL)
        return NULL;

    dirp = mc_opendir (vpath);
    if (dirp != NULL)
    {
        struct vfs_dirent *dp;

        for (dp = mc_readdir (dirp); dp != NULL; dp = mc_readdir (dirp))
            if (!DIR_IS_DOT (dp->d_name) && !DIR_IS_DOTDOT (dp->d_name))
            {
                vfs_path_t *tmp_vpath;

                tmp_vpath = vfs_path_append_new (vpath, dp->d_name, (char *) NULL);
                if (mc_lstat (tmp_vpath, &buf) != -1 && S_ISDIR (buf.st_mode))
                    tree_store_mark_checked (dp->d_name);
                vfs_path_free (tmp_vpath, TRUE);
            }

        mc_closedir (dirp);
    }
    tree_store_end_check ();
    entry->scanned = TRUE;

    return entry;
}

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