/*
 * wc_mergeinfo.c -- Query and store the mergeinfo.
 *
 * ====================================================================
 *    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.
 * ====================================================================
 */

/* ==================================================================== */



/*** Includes. ***/

#include "svn_cmdline.h"
#include "svn_pools.h"
#include "svn_client.h"
#include "svn_string.h"
#include "svn_sorts.h"
#include "svn_dirent_uri.h"
#include "svn_props.h"
#include "svn_hash.h"

#include "mergeinfo-normalizer.h"

#include "private/svn_fspath.h"
#include "private/svn_opt_private.h"
#include "private/svn_sorts_private.h"
#include "private/svn_subr_private.h"
#include "svn_private_config.h"



/* Our internal mergeinfo structure
 * It decorates the standard svn_mergeinfo_t with path and parent info. */
typedef struct mergeinfo_t
{
  /* The abspath of the working copy node that has this MERGINFO. */
  const char *local_path;

  /* The corresponding FS path. */
  const char *fs_path;

  /* The full URL of that node in the repository. */
  const char *url;

  /* Pointer to the closest parent mergeinfo that we found in the working
   * copy.  May be NULL. */
  struct mergeinfo_t *parent;

  /* All mergeinfo_t* who's PARENT points to this.  May be NULL. */
  apr_array_header_t *children;

  /* The parsed mergeinfo. */
  svn_mergeinfo_t mergeinfo;
} mergeinfo_t;

/* Parse the mergeinfo in PROPS as returned by svn_client_propget5,
 * construct our internal mergeinfo representation, allocated in
 * RESULT_POOL from it and return it *RESULT_P.  Use SCRATCH_POOL for
 * temporary allocations. */
static svn_error_t *
parse_mergeinfo(apr_array_header_t **result_p,
                apr_hash_t *props,
                apr_pool_t *result_pool,
                apr_pool_t *scratch_pool)
{
  apr_array_header_t *result = apr_array_make(result_pool,
                                              apr_hash_count(props),
                                              sizeof(mergeinfo_t *));
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
  apr_hash_index_t *hi;

  for (hi = apr_hash_first(scratch_pool, props); hi; hi = apr_hash_next(hi))
    {
      mergeinfo_t *entry = apr_pcalloc(result_pool, sizeof(*entry));
      svn_mergeinfo_t mergeinfo;
      svn_string_t *mi_string = apr_hash_this_val(hi);

      svn_pool_clear(iterpool);
      SVN_ERR(svn_mergeinfo_parse(&mergeinfo, mi_string->data, iterpool));

      entry->local_path = apr_pstrdup(result_pool, apr_hash_this_key(hi));
      entry->mergeinfo = svn_mergeinfo_dup(mergeinfo, result_pool);

      APR_ARRAY_PUSH(result, mergeinfo_t *) = entry;
    }

  svn_pool_destroy(iterpool);
  *result_p = result;

  return SVN_NO_ERROR;
}

/* Ordering function comparing two mergeinfo_t * by local abspath. */
static int
compare_mergeinfo(const void *lhs,
                  const void *rhs)
{
  const mergeinfo_t *lhs_mi = *(const mergeinfo_t *const *)lhs;
  const mergeinfo_t *rhs_mi = *(const mergeinfo_t *const *)rhs;

  return strcmp(lhs_mi->local_path, rhs_mi->local_path);
}

/* Implements svn_client_info_receiver2_t.
 * Updates the mergeinfo_t * given as BATON with the incoming INFO. */
static svn_error_t *
get_urls(void *baton,
         const char *target,
         const svn_client_info2_t *info,
         apr_pool_t *pool)
{
  mergeinfo_t *mi = baton;
  apr_pool_t *target_pool = apr_hash_pool_get(mi->mergeinfo);
  const char *rel_path = svn_uri_skip_ancestor(info->repos_root_URL,
                                               info->URL, pool);

  mi->url = apr_pstrdup(target_pool, info->URL);
  mi->fs_path = svn_fspath__canonicalize(rel_path, target_pool);

  return SVN_NO_ERROR;
}

/* Sort the nodes in MERGEINFO, sub-nodes first, add working copy info to
 * it and link nodes to their respective closest parents.  BATON provides
 * the client context.  SCRATCH_POOL is used for temporaries. */
static svn_error_t *
link_parents(apr_array_header_t *mergeinfo,
             svn_min__cmd_baton_t *baton,
             apr_pool_t *scratch_pool)
{
  apr_pool_t *result_pool = mergeinfo->pool;
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
  int i;

  /* We further down assume that there are is least one entry. */
  if (mergeinfo->nelts == 0)
    return SVN_NO_ERROR;

  /* sort mergeinfo by path */
  svn_sort__array(mergeinfo, compare_mergeinfo);

  /* add URL info */
  for (i = 0; i < mergeinfo->nelts; ++i)
    {
      mergeinfo_t *entry = APR_ARRAY_IDX(mergeinfo, i, mergeinfo_t *);
      const svn_opt_revision_t rev_working = { svn_opt_revision_working };

      svn_pool_clear(iterpool);
      SVN_ERR(svn_client_info4(entry->local_path, &rev_working,
                               &rev_working, svn_depth_empty, FALSE,
                               TRUE, FALSE, NULL, get_urls, entry,
                               baton->ctx, iterpool));
    }

  /* link all mergeinfo to their parent merge info - if that exists */
  for (i = 1; i < mergeinfo->nelts; ++i)
    {
      mergeinfo_t *entry = APR_ARRAY_IDX(mergeinfo, i, mergeinfo_t *);
      entry->parent = APR_ARRAY_IDX(mergeinfo, i - 1, mergeinfo_t *);

      while (   entry->parent
             && !svn_dirent_is_ancestor(entry->parent->local_path,
                                        entry->local_path))
        entry->parent = entry->parent->parent;

      /* Reverse pointer. */
      if (entry->parent)
        {
          if (!entry->parent->children)
            entry->parent->children
              = apr_array_make(result_pool, 4, sizeof(svn_mergeinfo_t));

          APR_ARRAY_PUSH(entry->parent->children, svn_mergeinfo_t)
            = entry->mergeinfo;
        }
    }

  /* break links for switched paths */
  for (i = 1; i < mergeinfo->nelts; ++i)
    {
      mergeinfo_t *entry = APR_ARRAY_IDX(mergeinfo, i, mergeinfo_t *);
      if (entry->parent)
        {
          if (!svn_uri__is_ancestor(entry->parent->url, entry->url))
            entry->parent = NULL;
        }
    }

  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

svn_error_t *
svn_min__read_mergeinfo(apr_array_header_t **result,
                        svn_min__cmd_baton_t *baton,
                        apr_pool_t *result_pool,
                        apr_pool_t *scratch_pool)
{
  svn_min__opt_state_t *opt_state = baton->opt_state;
  svn_client_ctx_t *ctx = baton->ctx;

  /* Pools for temporary data - to be cleaned up asap as they
   * significant amounts of it. */
  apr_pool_t *props_pool = svn_pool_create(scratch_pool);
  apr_pool_t *props_scratch_pool = svn_pool_create(scratch_pool);
  apr_hash_t *props;

  const svn_opt_revision_t rev_working = { svn_opt_revision_working };

  if (!baton->opt_state->quiet)
    SVN_ERR(svn_cmdline_printf(scratch_pool,
                              _("Scanning working copy %s ...\n"),
                              baton->local_abspath));

  SVN_ERR(svn_client_propget5(&props, NULL, SVN_PROP_MERGEINFO,
                              baton->local_abspath, &rev_working,
                              &rev_working, NULL,
                              opt_state->depth, NULL, ctx,
                              props_pool, props_scratch_pool));
  svn_pool_destroy(props_scratch_pool);

  SVN_ERR(parse_mergeinfo(result, props, result_pool, scratch_pool));
  svn_pool_destroy(props_pool);

  SVN_ERR(link_parents(*result, baton, scratch_pool));

  if (!baton->opt_state->quiet)
    SVN_ERR(svn_min__print_mergeinfo_stats(*result, scratch_pool));

  return SVN_NO_ERROR;
}

const char *
svn_min__common_parent(apr_array_header_t *mergeinfo,
                       apr_pool_t *result_pool,
                       apr_pool_t *scratch_pool)
{
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
  const char *result = NULL;
  int i;

  for (i = 0; i < mergeinfo->nelts; ++i)
    {
      apr_hash_index_t *hi;
      mergeinfo_t *entry = APR_ARRAY_IDX(mergeinfo, i, mergeinfo_t *);

      svn_pool_clear(iterpool);

      /* Make common base path cover the wc's FS path. */
      if (result == NULL)
        result = apr_pstrdup(result_pool, entry->fs_path);
      else if (!svn_dirent_is_ancestor(result, entry->fs_path))
        result = svn_dirent_get_longest_ancestor(result, entry->fs_path,
                                                 result_pool);

      /* Cover the branch FS paths mentioned in the mergeinfo. */
      for (hi = apr_hash_first(scratch_pool, entry->mergeinfo);
           hi;
           hi = apr_hash_next(hi))
        {
          const char * path = apr_hash_this_key(hi);
          if (!svn_dirent_is_ancestor(result, path))
            result = svn_dirent_get_longest_ancestor(result, path,
                                                     result_pool);
        }
    }

  svn_pool_destroy(iterpool);
  return result;
}

void
svn_min__get_mergeinfo_pair(const char **fs_path,
                            const char **parent_path,
                            const char **subtree_relpath,
                            svn_mergeinfo_t *parent_mergeinfo,
                            svn_mergeinfo_t *subtree_mergeinfo,
                            apr_array_header_t **siblings_mergeinfo,
                            apr_array_header_t *mergeinfo,
                            int idx)
{
  mergeinfo_t *entry;
  if (idx < 0 || mergeinfo->nelts <= idx)
    {
      *fs_path = "";
      *parent_path = "";
      *subtree_relpath = "";
      *parent_mergeinfo = NULL;
      *subtree_mergeinfo = NULL;
      *siblings_mergeinfo = NULL;

      return;
    }

  entry = APR_ARRAY_IDX(mergeinfo, idx, mergeinfo_t *);
  *fs_path = entry->fs_path;
  *subtree_mergeinfo = entry->mergeinfo;

  if (!entry->parent)
    {
      *parent_path = entry->local_path;
      *subtree_relpath = "";
      *parent_mergeinfo = NULL;
      *siblings_mergeinfo = NULL;

      return;
    }

  *parent_path = entry->parent->local_path;
  *subtree_relpath = svn_dirent_skip_ancestor(entry->parent->local_path,
                                              entry->local_path);
  *parent_mergeinfo = entry->parent->mergeinfo;
  *siblings_mergeinfo = entry->parent->children;
}

svn_mergeinfo_t
svn_min__get_mergeinfo(apr_array_header_t *mergeinfo,
                       int idx)
{
  SVN_ERR_ASSERT_NO_RETURN(idx >= 0 && idx < mergeinfo->nelts);
  return APR_ARRAY_IDX(mergeinfo, idx, mergeinfo_t *)->mergeinfo;
}

svn_error_t *
svn_min__sibling_ranges(apr_hash_t **sibling_ranges,
                        apr_array_header_t *sibling_mergeinfo,
                        const char *parent_path,
                        svn_rangelist_t *relevant_ranges,
                        apr_pool_t *result_pool,
                        apr_pool_t *scratch_pool)
{
  int i;
  apr_hash_t *result = svn_hash__make(result_pool);
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);

  for (i = 0; i < sibling_mergeinfo->nelts; ++i)
    {
      svn_mergeinfo_t mergeinfo;
      apr_hash_index_t *hi;

      svn_pool_clear(iterpool);
      mergeinfo = APR_ARRAY_IDX(sibling_mergeinfo, i, svn_mergeinfo_t);

      for (hi = apr_hash_first(iterpool, mergeinfo);
           hi;
           hi = apr_hash_next(hi))
        {
          const char *path = apr_hash_this_key(hi);
          if (svn_dirent_is_ancestor(parent_path, path))
            {
              svn_rangelist_t *common, *ranges = apr_hash_this_val(hi);
              SVN_ERR(svn_rangelist_intersect(&common, ranges,
                                              relevant_ranges, TRUE,
                                              result_pool));

              if (common->nelts)
                {
                  svn_hash_sets(result, apr_pstrdup(result_pool, path),
                                common);
                }
            }
        }
    }

  svn_pool_destroy(iterpool);
  *sibling_ranges = result;

  return SVN_NO_ERROR;
}

svn_error_t *
svn_min__write_mergeinfo(svn_min__cmd_baton_t *baton,
                         apr_array_header_t *mergeinfo,
                         apr_pool_t *scratch_pool)
{
  svn_client_ctx_t *ctx = baton->ctx;

  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
  int i;

  for (i = 0; i < mergeinfo->nelts; ++i)
    {
      mergeinfo_t *entry = APR_ARRAY_IDX(mergeinfo, i, mergeinfo_t *);
      svn_string_t *propval = NULL;
      apr_array_header_t *targets;

      svn_pool_clear(iterpool);

      targets = apr_array_make(iterpool, 1, sizeof(const char *));
      APR_ARRAY_PUSH(targets, const char *) = entry->local_path;

      /* If the mergeinfo is empty, keep the NULL PROPVAL to actually
       * delete the property. */
      if (apr_hash_count(entry->mergeinfo))
        SVN_ERR(svn_mergeinfo_to_string(&propval, entry->mergeinfo,
                                        iterpool));

      SVN_ERR(svn_client_propset_local(SVN_PROP_MERGEINFO, propval, targets,
                                       svn_depth_empty, FALSE, NULL, ctx,
                                       iterpool));
    }

  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

svn_error_t *
svn_min__remove_empty_mergeinfo(apr_array_header_t *mergeinfo)
{
  int i;
  int dest;

  for (i = 0, dest = 0; i < mergeinfo->nelts; ++i)
    {
      mergeinfo_t *entry = APR_ARRAY_IDX(mergeinfo, i, mergeinfo_t *);
      if (apr_hash_count(entry->mergeinfo))
        {
          APR_ARRAY_IDX(mergeinfo, dest, mergeinfo_t *) = entry;
          ++dest;
        }
    }

  mergeinfo->nelts = dest;

  return SVN_NO_ERROR;
}

svn_error_t *
svn_min__print_mergeinfo_stats(apr_array_header_t *wc_mergeinfo,
                               apr_pool_t *scratch_pool)
{
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);

  int branch_count = 0;
  int range_count = 0;

  /* Aggregate numbers. */
  int i;
  for (i = 0; i < wc_mergeinfo->nelts; ++i)
    {
      apr_hash_index_t *hi;
      svn_mergeinfo_t mergeinfo = svn_min__get_mergeinfo(wc_mergeinfo, i);

      svn_pool_clear(iterpool);

      branch_count += apr_hash_count(mergeinfo);

      for (hi = apr_hash_first(iterpool, mergeinfo);
           hi;
           hi = apr_hash_next(hi))
        {
          svn_rangelist_t *ranges = apr_hash_this_val(hi);
          range_count += ranges->nelts;
        }
    }

  /* Show them. */
  SVN_ERR(svn_cmdline_printf(scratch_pool,
                             _("    Found mergeinfo on %d nodes.\n"),
                             wc_mergeinfo->nelts));
  SVN_ERR(svn_cmdline_printf(scratch_pool,
                             _("    Found %d branch entries.\n"),
                             branch_count));
  SVN_ERR(svn_cmdline_printf(scratch_pool,
                             _("    Found %d merged revision ranges.\n\n"),
                             range_count));

  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

