/*
 * entries-dump.c :  dump pre-1.6 svn_wc_* output for python
 *
 * ====================================================================
 *    Licensed to the Apache Software Foundation (ASF) under one
 *    or more contributor license agreements.  See the NOTICE file
 *    distributed with this work for additional information
 *    regarding copyright ownership.  The ASF licenses this file
 *    to you under the Apache License, Version 2.0 (the
 *    "License"); you may not use this file except in compliance
 *    with the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing,
 *    software distributed under the License is distributed on an
 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *    KIND, either express or implied.  See the License for the
 *    specific language governing permissions and limitations
 *    under the License.
 * ====================================================================
 */

#include <stdlib.h>
#include <stdio.h>
#include <assert.h>

#include <apr_pools.h>
#include <apr_general.h>

#define SVN_DEPRECATED

#include "svn_types.h"
#include "svn_cmdline.h"
#include "svn_pools.h"
#include "svn_wc.h"
#include "svn_dirent_uri.h"
#include "svn_xml.h"

#include "private/svn_wc_private.h"

#include "../../libsvn_wc/wc.h"
#include "../../libsvn_wc/lock.h"

static void
print_prefix(void)
{
  puts("if b'' == '':\n"
       "  def _to_str(s):\n"
       "    return s\n"
       "else:\n"
       "  def _to_str(s):\n"
       "    return s.decode('utf-8', 'surrogateescape')\n"
       "\n"
       "class Entry(object):\n"
       "  \"\"\"An Entry object represents one node's entry in a pre-1.6"
       " .svn/entries file.\n\n"
       "Similar to #svn_wc_entry_t, but not all fields are populated.\n\n"
       "Entry objects are generated by the 'entries-dump'"
       " test helper tool.\"\"\"\n\n"
       "  if b'' == '':\n"
       "    def set_strval(self, name, val):\n"
       "      self.__setattr__(name, val)\n"
       "  else:\n"
       "    def set_strval(self, name, val):\n"
       "      global _to_str\n"
       "      self.__setattr__(name, _to_str(val))\n");
}

static void
print_as_bytes(const char *val)
{
  printf("b'");
  while(*val)
    {
      printf("\\x%02x", (unsigned int)(unsigned char)*val++);
    }
  printf("'");
}

static void
str_value(const char *name, const char *value, apr_pool_t *pool)
{
  if (value == NULL)
    printf("e.%s = None\n", name);
  else
    {
      svn_stringbuf_t *escaped_value = NULL;
      svn_xml_escape_attr_cstring(&escaped_value, value, pool);

      /* Print the human-readable value. */
      assert(NULL == strchr(escaped_value->data, '\n'));
      printf("# e.%s = '%s'\n", name, escaped_value->data);

      /* Print the machine-readable value. */
      printf("e.set_strval('%s', ", name);
      print_as_bytes(value);
      printf(")\n");
    }
}

static void
int_value(const char *name, long int value)
{
  printf("e.%s = %ld\n", name, value);
}


static void
bool_value(const char *name, svn_boolean_t value)
{
  if (value)
    printf("e.%s = True\n", name);
  else
    printf("e.%s = False\n", name);
}

static svn_error_t *
entries_dump(const char *dir_path, svn_wc_adm_access_t *related, apr_pool_t *pool)
{
  svn_wc_adm_access_t *adm_access = NULL;
  apr_hash_t *entries;
  apr_hash_index_t *hi;
  svn_boolean_t locked;
  svn_error_t *err;
  svn_wc_context_t *wc_ctx = NULL;
  const char *dir_abspath;
  apr_pool_t *iterpool = svn_pool_create(pool); 

  SVN_ERR(svn_dirent_get_absolute(&dir_abspath, dir_path, pool));

  err = svn_wc_adm_open3(&adm_access, related, dir_path, FALSE, 0,
                         NULL, NULL, pool);
  if (!err)
    {
      SVN_ERR(svn_wc__context_create_with_db(&wc_ctx, NULL,
                                             svn_wc__adm_get_db(adm_access),
                                             pool));

      SVN_ERR(svn_wc_locked2(NULL, &locked, wc_ctx, dir_abspath, pool));
      SVN_ERR(svn_wc_entries_read(&entries, adm_access, TRUE, pool));
    }
  else if (err && err->apr_err == SVN_ERR_WC_LOCKED
           && related
           && ! strcmp(dir_path, svn_wc_adm_access_path(related)))
    {
      /* Common caller error: Can't open a baton when there is one. */
      svn_error_clear(err);

      SVN_ERR(svn_wc__context_create_with_db(&wc_ctx, NULL,
                                             svn_wc__adm_get_db(related),
                                             pool));

      SVN_ERR(svn_wc_locked2(NULL, &locked, wc_ctx, dir_abspath, pool));
      SVN_ERR(svn_wc_entries_read(&entries, related, TRUE, pool));
    }
  else
    {
      const char *lockfile_path;
      svn_node_kind_t kind;

      /* ### Should svn_wc_adm_open3 be returning UPGRADE_REQUIRED? */
      if (err->apr_err != SVN_ERR_WC_NOT_DIRECTORY)
        return err;
      svn_error_clear(err);
      adm_access = NULL;
      SVN_ERR(svn_wc__read_entries_old(&entries, dir_abspath, pool, pool));
      lockfile_path = svn_dirent_join_many(pool, dir_path,
                                           svn_wc_get_adm_dir(pool),
                                           "lock", SVN_VA_NULL);
      SVN_ERR(svn_io_check_path(lockfile_path, &kind, pool));
      locked = (kind == svn_node_file);
    }

  for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi))
    {
      svn_stringbuf_t *escaped_key;
      const char *key = apr_hash_this_key(hi);
      const svn_wc_entry_t *entry = apr_hash_this_val(hi);

      svn_pool_clear(iterpool); 
      SVN_ERR_ASSERT(strcmp(key, entry->name) == 0);
      printf("e = Entry()\n");
      str_value("name", entry->name, iterpool);
      int_value("revision", entry->revision);
      str_value("url", entry->url, iterpool);
      str_value("repos", entry->repos, iterpool);
      str_value("uuid", entry->uuid, iterpool);
      int_value("kind", entry->kind);
      int_value("schedule", entry->schedule);
      bool_value("copied", entry->copied);
      bool_value("deleted", entry->deleted);
      bool_value("absent", entry->absent);
      bool_value("incomplete", entry->incomplete);
      str_value("copyfrom_url", entry->copyfrom_url, iterpool);
      int_value("copyfrom_rev", entry->copyfrom_rev);
      str_value("conflict_old", entry->conflict_old, iterpool);
      str_value("conflict_new", entry->conflict_new, iterpool);
      str_value("conflict_wrk", entry->conflict_wrk, iterpool);
      str_value("prejfile", entry->prejfile, iterpool);
      /* skip: text_time */
      /* skip: prop_time */
      /* skip: checksum */
      int_value("cmt_rev", entry->cmt_rev);
      /* skip: cmt_date */
      str_value("cmt_author", entry->cmt_author, iterpool);
      str_value("lock_token", entry->lock_token, iterpool);
      str_value("lock_owner", entry->lock_owner, iterpool);
      str_value("lock_comment", entry->lock_comment, iterpool);
      /* skip: lock_creation_date */
      /* skip: has_props */
      /* skip: has_prop_mods */
      /* skip: cachable_props */
      /* skip: present_props */
      str_value("changelist", entry->changelist, iterpool);
      /* skip: working_size */
      /* skip: keep_local */
      int_value("depth", entry->depth);
      /* skip: tree_conflict_data */
      bool_value("file_external", entry->file_external_path != NULL);
      /* skip: file_external_peg_rev */
      /* skip: file_external_rev */
      bool_value("locked", locked && *entry->name == '\0');
      /* Print the human-readable value. */
      escaped_key = NULL;
      svn_xml_escape_attr_cstring(&escaped_key, key, iterpool);
      assert(NULL == strchr(escaped_key->data, '\n'));
      printf("# entries['%s'] = e\n", escaped_key->data);
      /* Print the machine-readable value. */
      printf("entries[_to_str(");
      print_as_bytes(key);
      printf(")] = e\n");
    }
  svn_pool_destroy(iterpool);

  if (wc_ctx)
    SVN_ERR(svn_wc_context_destroy(wc_ctx));

  if (adm_access)
    SVN_ERR(svn_wc_adm_close2(adm_access, pool));

  return SVN_NO_ERROR;
}


/* baton for print_dir */
struct directory_walk_baton
{
  svn_wc_context_t *wc_ctx;
  const char *root_abspath;
  const char *prefix_path;
  svn_wc_adm_access_t *adm_access;
};

/* svn_wc__node_found_func_t implementation for directory_dump */
static svn_error_t *
print_dir(const char *local_abspath,
          svn_node_kind_t kind,
          void *walk_baton,
          apr_pool_t *scratch_pool)
{
  struct directory_walk_baton *bt = walk_baton;
  const char *path;

  if (kind != svn_node_dir)
    return SVN_NO_ERROR;

  /* If LOCAL_ABSPATH a child of or equal to ROOT_ABSPATH, then display
     a relative path starting with PREFIX_PATH. */
  path = svn_dirent_skip_ancestor(bt->root_abspath, local_abspath);
  if (path)
    path = svn_dirent_join(bt->prefix_path, path, scratch_pool);
  else
    path = local_abspath;
  printf("%s\n", svn_dirent_local_style(path, scratch_pool));

  return SVN_NO_ERROR;
}

static svn_error_t *
directory_dump_old(struct directory_walk_baton *bt,
                   const char *dir_abspath,
                   apr_pool_t *scratch_pool)
{
  apr_hash_t *entries;
  apr_hash_index_t *hi;

  SVN_ERR(svn_wc__read_entries_old(&entries, dir_abspath,
                                   scratch_pool, scratch_pool));
  for (hi = apr_hash_first(scratch_pool, entries); hi; hi = apr_hash_next(hi))
    {
      const svn_wc_entry_t *entry = apr_hash_this_val(hi);
      const char *local_abspath;

      if (entry->deleted || entry->absent || entry->kind != svn_node_dir)
        continue;

      local_abspath = svn_dirent_join(dir_abspath, entry->name, scratch_pool);
      if (strcmp(entry->name, SVN_WC_ENTRY_THIS_DIR))
        SVN_ERR(directory_dump_old(bt, local_abspath, scratch_pool));
      else
        SVN_ERR(print_dir(local_abspath, entry->kind, bt, scratch_pool));
    }
  return SVN_NO_ERROR;
}

/* Print all not-hidden subdirectories in the working copy, starting by path */
static svn_error_t *
directory_dump(const char *path,
               apr_pool_t *scratch_pool)
{
  struct directory_walk_baton bt;
  svn_error_t *err;

  SVN_ERR(svn_wc_context_create(&bt.wc_ctx, NULL, scratch_pool, scratch_pool));
  SVN_ERR(svn_dirent_get_absolute(&bt.root_abspath, path, scratch_pool));

  bt.prefix_path = path;

  err = svn_wc__internal_walk_children(bt.wc_ctx->db, bt.root_abspath, FALSE,
                                       NULL, print_dir, &bt, svn_depth_infinity,
                                       NULL, NULL, scratch_pool);
  if (err)
    {
      const char *dir_abspath;

      if (err->apr_err != SVN_ERR_WC_UPGRADE_REQUIRED)
        return err;
      svn_error_clear(err);
      SVN_ERR(svn_dirent_get_absolute(&dir_abspath, path, scratch_pool));
      SVN_ERR(directory_dump_old(&bt, dir_abspath, scratch_pool));
    }

  return svn_error_trace(svn_wc_context_destroy(bt.wc_ctx));
}

static svn_error_t *
tree_dump_dir(const char *local_abspath,
              svn_node_kind_t kind,
              void *walk_baton,
              apr_pool_t *scratch_pool)
{
  struct directory_walk_baton *bt = walk_baton;
  const char *path;
  svn_stringbuf_t *escaped_path;

  if (kind != svn_node_dir)
    return SVN_NO_ERROR;

  if (strcmp(local_abspath, bt->root_abspath) != 0)
    {
      svn_boolean_t is_wcroot;
      SVN_ERR(svn_wc__db_is_wcroot(&is_wcroot, bt->wc_ctx->db,
                                   local_abspath, scratch_pool));

      if (is_wcroot)
        return SVN_NO_ERROR; /* Report the stub, but not the data */
    }

  /* If LOCAL_ABSPATH a child of or equal to ROOT_ABSPATH, then display
     a relative path starting with PREFIX_PATH. */
  path = svn_dirent_skip_ancestor(bt->root_abspath, local_abspath);
  if (path)
    path = svn_dirent_join(bt->prefix_path, path, scratch_pool);
  else
    path = local_abspath;

  printf("entries = {}\n");
  SVN_ERR(entries_dump(path, bt->adm_access, scratch_pool));

  /* Print the human-readable value. */
  escaped_path = NULL;
  svn_xml_escape_attr_cstring(&escaped_path, path, scratch_pool);
  assert(NULL == strchr(escaped_path->data, '\n'));
  printf("# dirs['%s'] = entries\n", escaped_path->data);
  /* Print the machine-readable value. */
  printf("dirs[_to_str(");
  print_as_bytes(path);
  printf(")] = entries\n");
  return SVN_NO_ERROR;

}

static svn_error_t *
tree_dump(const char *path,
          apr_pool_t *scratch_pool)
{
  struct directory_walk_baton bt;
  svn_sqlite__db_t *sdb;
  svn_wc__db_t *db;

  bt.prefix_path = path;

  /* Obtain an access baton to allow re-using the same wc_db for all access */
  SVN_ERR(svn_wc_adm_open3(&bt.adm_access, NULL, path, FALSE, 0, NULL, NULL,
                           scratch_pool));

  db = svn_wc__adm_get_db(bt.adm_access);

  SVN_ERR(svn_wc__context_create_with_db(&bt.wc_ctx, NULL, db, scratch_pool));

  SVN_ERR(svn_dirent_get_absolute(&bt.root_abspath, path, scratch_pool));

  /* And now get us a transaction on the database to avoid obtaining and
     releasing locks all the time */
  SVN_ERR(svn_wc__db_temp_borrow_sdb(&sdb, bt.wc_ctx->db, bt.root_abspath,
                                     scratch_pool));

  SVN_SQLITE__WITH_LOCK(
      svn_wc__internal_walk_children(db, bt.root_abspath, FALSE,
                                     NULL, tree_dump_dir, &bt,
                                     svn_depth_infinity,
                                     NULL, NULL, scratch_pool),
      sdb);

  /* And close everything we've opened */
  SVN_ERR(svn_wc_context_destroy(bt.wc_ctx));
  SVN_ERR(svn_wc_adm_close2(bt.adm_access, scratch_pool));

  return SVN_NO_ERROR;
}

int
main(int argc, const char *argv[])
{
  apr_pool_t *pool;
  int exit_code = EXIT_SUCCESS;
  svn_error_t *err;
  const char *path;
  const char *cmd;

  if (argc < 2 || argc > 4)
    {
      fprintf(stderr, "USAGE: entries-dump [--entries|--subdirs|--tree-dump] DIR_PATH\n");
      exit(1);
    }

  if (svn_cmdline_init("entries-dump", stderr) != EXIT_SUCCESS)
    {
      return EXIT_FAILURE;
    }

  /* Create our top-level pool.  Use a separate mutexless allocator,
   * given this application is single threaded.
   */
  pool = apr_allocator_owner_get(svn_pool_create_allocator(FALSE));

  path = svn_dirent_internal_style(argv[argc-1], pool);

  if (argc > 2)
    cmd = argv[1];
  else
    cmd = NULL;

  if (!cmd || !strcmp(cmd, "--entries"))
    {
      print_prefix();
      err = entries_dump(path, NULL, pool);
    }
  else if (!strcmp(cmd, "--subdirs"))
    {
      err = directory_dump(path, pool);
    }
  else if (!strcmp(cmd, "--tree-dump"))
    {
      print_prefix();
      err = tree_dump(path, pool);
    }
  else
    {
      err = svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                              "Invalid command '%s'",
                              cmd);
    }
  if (err)
    {
      svn_handle_error2(err, stderr, FALSE, "entries-dump: ");
      svn_error_clear(err);
      exit_code = EXIT_FAILURE;
    }

  /* Clean up, and get outta here */
  svn_pool_destroy(pool);
  apr_terminate();

  return exit_code;
}
