/*
 * diff_local.c: comparing local trees with each other
 *
 * ====================================================================
 *    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 <apr_strings.h>
#include <apr_pools.h>
#include <apr_hash.h>
#include "svn_hash.h"
#include "svn_types.h"
#include "svn_wc.h"
#include "svn_diff.h"
#include "svn_client.h"
#include "svn_string.h"
#include "svn_error.h"
#include "svn_dirent_uri.h"
#include "svn_io.h"
#include "svn_pools.h"
#include "svn_props.h"
#include "svn_sorts.h"
#include "svn_subst.h"
#include "client.h"

#include "private/svn_sorts_private.h"
#include "private/svn_wc_private.h"
#include "private/svn_diff_tree.h"

#include "svn_private_config.h"


/* Try to get properties for LOCAL_ABSPATH and return them in the property
 * hash *PROPS. If there are no properties because LOCAL_ABSPATH is not
 * versioned, return an empty property hash. */
static svn_error_t *
get_props(apr_hash_t **props,
          const char *local_abspath,
          svn_wc_context_t *wc_ctx,
          apr_pool_t *result_pool,
          apr_pool_t *scratch_pool)
{
  svn_error_t *err;

  err = svn_wc_prop_list2(props, wc_ctx, local_abspath, result_pool,
                          scratch_pool);
  if (err)
    {
      if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND ||
          err->apr_err == SVN_ERR_WC_NOT_WORKING_COPY)
        {
          svn_error_clear(err);
          *props = apr_hash_make(result_pool);

          /* ### Apply autoprops, like 'svn add' would? */
        }
      else
        return svn_error_trace(err);
    }

  return SVN_NO_ERROR;
}

/* Forward declaration */
static svn_error_t *
do_file_diff(const char *left_abspath,
             const char *right_abspath,
             const char *left_root_abspath,
             const char *right_root_abspath,
             svn_boolean_t left_only,
             svn_boolean_t right_only,
             void *parent_baton,
             const svn_diff_tree_processor_t *diff_processor,
             svn_client_ctx_t *ctx,
             apr_pool_t *scratch_pool);

/* Forward declaration */
static svn_error_t *
do_dir_diff(const char *left_abspath,
            const char *right_abspath,
            const char *left_root_abspath,
            const char *right_root_abspath,
            svn_boolean_t left_only,
            svn_boolean_t right_only,
            svn_boolean_t left_before_right,
            svn_depth_t depth,
            void *parent_baton,
            const svn_diff_tree_processor_t *diff_processor,
            svn_client_ctx_t *ctx,
            apr_pool_t *scratch_pool);

/* Produce a diff of depth DEPTH between two arbitrary directories at
 * LEFT_ABSPATH1 and RIGHT_ABSPATH2, using the provided diff callbacks
 * to show file changes and, for versioned nodes, property changes.
 *
 * Report paths as relative from LEFT_ROOT_ABSPATH/RIGHT_ROOT_ABSPATH.
 *
 * If LEFT_ONLY is TRUE, only the left source exists (= everything will
 * be reported as deleted). If RIGHT_ONLY is TRUE, only the right source
 * exists (= everything will be reported as added).
 *
 * If LEFT_BEFORE_RIGHT is TRUE and left and right are unrelated, left is
 * reported first. If false, right is reported first. (This is to allow
 * producing a proper inverse diff).
 *
 * Walk the sources according to depth, and report with parent baton
 * PARENT_BATON. */
static svn_error_t *
inner_dir_diff(const char *left_abspath,
               const char *right_abspath,
               const char *left_root_abspath,
               const char *right_root_abspath,
               svn_boolean_t left_only,
               svn_boolean_t right_only,
               svn_boolean_t left_before_right,
               svn_depth_t depth,
               void *parent_baton,
               const svn_diff_tree_processor_t *diff_processor,
               svn_client_ctx_t *ctx,
               apr_pool_t *scratch_pool)
{
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
  apr_hash_t *left_dirents;
  apr_hash_t *right_dirents;
  apr_array_header_t *sorted_dirents;
  svn_error_t *err;
  svn_depth_t depth_below_here;
  int i;

  SVN_ERR_ASSERT(depth >= svn_depth_files && depth <= svn_depth_infinity);

  if (!right_only)
    {
      err = svn_io_get_dirents3(&left_dirents, left_abspath, FALSE,
                                scratch_pool, iterpool);

      if (err && (APR_STATUS_IS_ENOENT(err->apr_err)
                  || SVN__APR_STATUS_IS_ENOTDIR(err->apr_err)))
        {
          svn_error_clear(err);
          left_dirents = apr_hash_make(scratch_pool);
          right_only = TRUE;
        }
      else
        SVN_ERR(err);
    }
  else
    left_dirents = apr_hash_make(scratch_pool);

  if (!left_only)
    {
      err = svn_io_get_dirents3(&right_dirents, right_abspath, FALSE,
                                scratch_pool, iterpool);

      if (err && (APR_STATUS_IS_ENOENT(err->apr_err)
                  || SVN__APR_STATUS_IS_ENOTDIR(err->apr_err)))
        {
          svn_error_clear(err);
          right_dirents = apr_hash_make(scratch_pool);
          left_only = TRUE;
        }
      else
        SVN_ERR(err);
    }
  else
    right_dirents = apr_hash_make(scratch_pool);

  if (left_only && right_only)
    return SVN_NO_ERROR; /* Somebody deleted the directory?? */

  if (depth != svn_depth_infinity)
    depth_below_here = svn_depth_empty;
  else
    depth_below_here = svn_depth_infinity;

  sorted_dirents = svn_sort__hash(apr_hash_merge(iterpool, left_dirents,
                                                 right_dirents, NULL, NULL),
                                  svn_sort_compare_items_as_paths,
                                  scratch_pool);

  for (i = 0; i < sorted_dirents->nelts; i++)
    {
      svn_sort__item_t* elt = &APR_ARRAY_IDX(sorted_dirents, i, svn_sort__item_t);
      svn_io_dirent2_t *left_dirent;
      svn_io_dirent2_t *right_dirent;
      const char *child_left_abspath;
      const char *child_right_abspath;

      svn_pool_clear(iterpool);

      if (ctx->cancel_func)
        SVN_ERR(ctx->cancel_func(ctx->cancel_baton));

      if (svn_wc_is_adm_dir(elt->key, iterpool))
        continue;

      left_dirent = right_only ? NULL : svn_hash_gets(left_dirents, elt->key);
      right_dirent = left_only ? NULL : svn_hash_gets(right_dirents, elt->key);

      child_left_abspath = svn_dirent_join(left_abspath, elt->key, iterpool);
      child_right_abspath = svn_dirent_join(right_abspath, elt->key, iterpool);

      if (((left_dirent == NULL) != (right_dirent == NULL))
          || (left_dirent->kind != right_dirent->kind))
        {
          /* Report delete and/or add */
          if (left_dirent && left_before_right)
            {
              if (left_dirent->kind == svn_node_file)
                SVN_ERR(do_file_diff(child_left_abspath, child_right_abspath,
                                     left_root_abspath, right_root_abspath,
                                     TRUE, FALSE, parent_baton,
                                     diff_processor, ctx, iterpool));
              else if (depth >= svn_depth_immediates)
                SVN_ERR(do_dir_diff(child_left_abspath, child_right_abspath,
                                    left_root_abspath, right_root_abspath,
                                    TRUE, FALSE, left_before_right,
                                    depth_below_here, parent_baton,
                                    diff_processor, ctx, iterpool));
            }

          if (right_dirent)
            {
              if (right_dirent->kind == svn_node_file)
                SVN_ERR(do_file_diff(child_left_abspath, child_right_abspath,
                                     left_root_abspath, right_root_abspath,
                                     FALSE, TRUE, parent_baton,
                                     diff_processor, ctx, iterpool));
              else if (depth >= svn_depth_immediates)
                SVN_ERR(do_dir_diff(child_left_abspath, child_right_abspath,
                                    left_root_abspath, right_root_abspath,
                                    FALSE, TRUE,  left_before_right,
                                    depth_below_here, parent_baton,
                                    diff_processor, ctx, iterpool));
            }

          if (left_dirent && !left_before_right)
            {
              if (left_dirent->kind == svn_node_file)
                SVN_ERR(do_file_diff(child_left_abspath, child_right_abspath,
                                     left_root_abspath, right_root_abspath,
                                     TRUE, FALSE, parent_baton,
                                     diff_processor, ctx, iterpool));
              else if (depth >= svn_depth_immediates)
                SVN_ERR(do_dir_diff(child_left_abspath, child_right_abspath,
                                    left_root_abspath, right_root_abspath,
                                    TRUE, FALSE,  left_before_right,
                                    depth_below_here, parent_baton,
                                    diff_processor, ctx, iterpool));
            }
        }
      else if (left_dirent->kind == svn_node_file)
        {
          /* Perform file-file diff */
          SVN_ERR(do_file_diff(child_left_abspath, child_right_abspath,
                               left_root_abspath, right_root_abspath,
                               FALSE, FALSE, parent_baton,
                               diff_processor, ctx, iterpool));
        }
      else if (depth >= svn_depth_immediates)
        {
          /* Perform dir-dir diff */
          SVN_ERR(do_dir_diff(child_left_abspath, child_right_abspath,
                              left_root_abspath, right_root_abspath,
                              FALSE, FALSE,  left_before_right,
                              depth_below_here, parent_baton,
                              diff_processor, ctx, iterpool));
        }
    }

  return SVN_NO_ERROR;
}

/* Translates *LEFT_ABSPATH to a temporary file if PROPS specify that the
   file needs translation. *LEFT_ABSPATH is updated to point to a file that
   lives at least as long as RESULT_POOL when translation is necessary.
   Otherwise the value is not updated */
static svn_error_t *
translate_if_necessary(const char **local_abspath,
                       apr_hash_t *props,
                       svn_cancel_func_t cancel_func,
                       void *cancel_baton,
                       apr_pool_t *result_pool,
                       apr_pool_t *scratch_pool)
{
  const svn_string_t *eol_style_val;
  const svn_string_t *keywords_val;
  svn_subst_eol_style_t eol_style;
  const char *eol;
  apr_hash_t *keywords;
  svn_stream_t *contents;
  svn_stream_t *dst;

  /* if (svn_hash_gets(props, SVN_PROP_SPECIAL))
      ### TODO: Implement */

  eol_style_val = svn_hash_gets(props, SVN_PROP_EOL_STYLE);
  keywords_val = svn_hash_gets(props, SVN_PROP_KEYWORDS);

  if (eol_style_val)
    svn_subst_eol_style_from_value(&eol_style, &eol, eol_style_val->data);
  else
    {
      eol = NULL;
      eol_style = svn_subst_eol_style_none;
    }

  if (keywords_val)
    SVN_ERR(svn_subst_build_keywords3(&keywords, keywords_val->data,
                                      APR_STRINGIFY(SVN_INVALID_REVNUM),
                                      "", "", 0, "", scratch_pool));
  else
    keywords = NULL;

  if (!svn_subst_translation_required(eol_style, eol, keywords, FALSE, FALSE))
    return SVN_NO_ERROR;

  SVN_ERR(svn_stream_open_readonly(&contents, *local_abspath,
                                    scratch_pool, scratch_pool));

  SVN_ERR(svn_stream_open_unique(&dst, local_abspath, NULL,
                                  svn_io_file_del_on_pool_cleanup,
                                  result_pool, scratch_pool));

  dst = svn_subst_stream_translated(dst, eol, TRUE /* repair */,
                                    keywords, FALSE /* expand */,
                                    scratch_pool);

  SVN_ERR(svn_stream_copy3(contents, dst, cancel_func, cancel_baton,
                           scratch_pool));

  return SVN_NO_ERROR;
}

/* Handles reporting of a file for inner_dir_diff */
static svn_error_t *
do_file_diff(const char *left_abspath,
             const char *right_abspath,
             const char *left_root_abspath,
             const char *right_root_abspath,
             svn_boolean_t left_only,
             svn_boolean_t right_only,
             void *parent_baton,
             const svn_diff_tree_processor_t *diff_processor,
             svn_client_ctx_t *ctx,
             apr_pool_t *scratch_pool)
{
  const char *relpath;
  svn_diff_source_t *left_source;
  svn_diff_source_t *right_source;
  svn_boolean_t skip = FALSE;
  apr_hash_t *left_props;
  apr_hash_t *right_props;
  void *file_baton;

  relpath = svn_dirent_skip_ancestor(left_root_abspath, left_abspath);

 if (! right_only)
    left_source = svn_diff__source_create(SVN_INVALID_REVNUM, scratch_pool);
  else
    left_source = NULL;

  if (! left_only)
    right_source = svn_diff__source_create(SVN_INVALID_REVNUM, scratch_pool);
  else
    right_source = NULL;

  SVN_ERR(diff_processor->file_opened(&file_baton, &skip,
                                      relpath,
                                      left_source,
                                      right_source,
                                      NULL /* copyfrom_source */,
                                      parent_baton,
                                      diff_processor,
                                      scratch_pool,
                                      scratch_pool));

  if (skip)
    return SVN_NO_ERROR;

   if (! right_only)
    {
      SVN_ERR(get_props(&left_props, left_abspath, ctx->wc_ctx,
                        scratch_pool, scratch_pool));

      /* We perform a mimetype detection to avoid diffing binary files
         for textual changes.*/
      if (! svn_hash_gets(left_props, SVN_PROP_MIME_TYPE))
        {
          const char *mime_type;

          /* ### Use libmagic magic? */
          SVN_ERR(svn_io_detect_mimetype2(&mime_type, left_abspath,
                                          ctx->mimetypes_map, scratch_pool));

          if (mime_type)
            svn_hash_sets(left_props, SVN_PROP_MIME_TYPE,
                          svn_string_create(mime_type, scratch_pool));
        }

      SVN_ERR(translate_if_necessary(&left_abspath, left_props,
                                     ctx->cancel_func, ctx->cancel_baton,
                                     scratch_pool, scratch_pool));
    }
  else
    left_props = NULL;

  if (! left_only)
    {
      SVN_ERR(get_props(&right_props, right_abspath, ctx->wc_ctx,
                        scratch_pool, scratch_pool));

      /* We perform a mimetype detection to avoid diffing binary files
         for textual changes.*/
      if (! svn_hash_gets(right_props, SVN_PROP_MIME_TYPE))
        {
          const char *mime_type;

          /* ### Use libmagic magic? */
          SVN_ERR(svn_io_detect_mimetype2(&mime_type, right_abspath,
                                          ctx->mimetypes_map, scratch_pool));

          if (mime_type)
            svn_hash_sets(right_props, SVN_PROP_MIME_TYPE,
                          svn_string_create(mime_type, scratch_pool));
        }

      SVN_ERR(translate_if_necessary(&right_abspath, right_props,
                                     ctx->cancel_func, ctx->cancel_baton,
                                     scratch_pool, scratch_pool));

    }
  else
    right_props = NULL;

  if (left_only)
    {
      SVN_ERR(diff_processor->file_deleted(relpath,
                                           left_source,
                                           left_abspath,
                                           left_props,
                                           file_baton,
                                           diff_processor,
                                           scratch_pool));
    }
  else if (right_only)
    {
      SVN_ERR(diff_processor->file_added(relpath,
                                         NULL /* copyfrom_source */,
                                         right_source,
                                         NULL /* copyfrom_file */,
                                         right_abspath,
                                         NULL /* copyfrom_props */,
                                         right_props,
                                         file_baton,
                                         diff_processor,
                                         scratch_pool));
    }
  else
    {
      /* ### Perform diff -> close/changed */
      svn_boolean_t same;
      apr_array_header_t *prop_changes;

      SVN_ERR(svn_io_files_contents_same_p(&same, left_abspath, right_abspath,
                                           scratch_pool));

      SVN_ERR(svn_prop_diffs(&prop_changes, right_props, left_props,
                             scratch_pool));

      if (!same || prop_changes->nelts > 0)
        {
          SVN_ERR(diff_processor->file_changed(relpath,
                                               left_source,
                                               right_source,
                                               same ? NULL : left_abspath,
                                               same ? NULL : right_abspath,
                                               left_props,
                                               right_props,
                                               !same,
                                               prop_changes,
                                               file_baton,
                                               diff_processor,
                                               scratch_pool));
        }
      else
        {
          SVN_ERR(diff_processor->file_closed(relpath,
                                            left_source,
                                            right_source,
                                            file_baton,
                                            diff_processor,
                                            scratch_pool));
        }
    }
  return SVN_NO_ERROR;
}


/* Handles reporting of a directory and its children for inner_dir_diff */
static svn_error_t *
do_dir_diff(const char *left_abspath,
            const char *right_abspath,
            const char *left_root_abspath,
            const char *right_root_abspath,
            svn_boolean_t left_only,
            svn_boolean_t right_only,
            svn_boolean_t left_before_right,
            svn_depth_t depth,
            void *parent_baton,
            const svn_diff_tree_processor_t *diff_processor,
            svn_client_ctx_t *ctx,
            apr_pool_t *scratch_pool)
{
  const char *relpath;
  svn_diff_source_t *left_source;
  svn_diff_source_t *right_source;
  svn_boolean_t skip = FALSE;
  svn_boolean_t skip_children = FALSE;
  void *dir_baton;
  apr_hash_t *left_props;
  apr_hash_t *right_props;

  relpath = svn_dirent_skip_ancestor(left_root_abspath, left_abspath);

  if (! right_only)
    {
      left_source = svn_diff__source_create(SVN_INVALID_REVNUM, scratch_pool);
      SVN_ERR(get_props(&left_props, left_abspath, ctx->wc_ctx,
                        scratch_pool, scratch_pool));
    }
  else
    {
      left_source = NULL;
      left_props = NULL;
    }

  if (! left_only)
    {
      right_source = svn_diff__source_create(SVN_INVALID_REVNUM, scratch_pool);
      SVN_ERR(get_props(&right_props, right_abspath, ctx->wc_ctx,
                        scratch_pool, scratch_pool));
    }
  else
    {
      right_source = NULL;
      right_props = NULL;
    }

  SVN_ERR(diff_processor->dir_opened(&dir_baton, &skip, &skip_children,
                                     relpath,
                                     left_source,
                                     right_source,
                                     NULL /* copyfrom_source */,
                                     parent_baton,
                                     diff_processor,
                                     scratch_pool, scratch_pool));

  if (!skip_children)
    {
      if (depth >= svn_depth_files)
        SVN_ERR(inner_dir_diff(left_abspath, right_abspath,
                               left_root_abspath, right_root_abspath,
                               left_only, right_only,
                               left_before_right, depth,
                               dir_baton,
                               diff_processor, ctx, scratch_pool));
    }
  else if (skip)
    return SVN_NO_ERROR;

  if (left_props && right_props)
    {
      apr_array_header_t *prop_diffs;

      SVN_ERR(svn_prop_diffs(&prop_diffs, right_props, left_props,
                             scratch_pool));

      if (prop_diffs->nelts)
        {
          SVN_ERR(diff_processor->dir_changed(relpath,
                                              left_source,
                                              right_source,
                                              left_props,
                                              right_props,
                                              prop_diffs,
                                              dir_baton,
                                              diff_processor,
                                              scratch_pool));
          return SVN_NO_ERROR;
        }
    }

  if (left_source && right_source)
    {
      SVN_ERR(diff_processor->dir_closed(relpath,
                                         left_source,
                                         right_source,
                                         dir_baton,
                                         diff_processor,
                                         scratch_pool));
    }
  else if (left_source)
    {
      SVN_ERR(diff_processor->dir_deleted(relpath,
                                          left_source,
                                          left_props,
                                          dir_baton,
                                          diff_processor,
                                          scratch_pool));
    }
  else
    {
      SVN_ERR(diff_processor->dir_added(relpath,
                                        NULL /* copyfrom_source */,
                                        right_source,
                                        NULL /* copyfrom_props */,
                                        right_props,
                                        dir_baton,
                                        diff_processor,
                                        scratch_pool));
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__arbitrary_nodes_diff(const char *left_abspath,
                                 const char *right_abspath,
                                 svn_depth_t depth,
                                 const svn_diff_tree_processor_t *diff_processor,
                                 svn_client_ctx_t *ctx,
                                 apr_pool_t *scratch_pool)
{
  svn_node_kind_t left_kind;
  svn_node_kind_t right_kind;
  const char *left_root_abspath = left_abspath;
  const char *right_root_abspath = right_abspath;
  svn_boolean_t left_before_right = TRUE; /* Future argument? */

  if (depth == svn_depth_unknown)
    depth = svn_depth_infinity;

  SVN_ERR(svn_io_check_resolved_path(left_abspath, &left_kind, scratch_pool));
  SVN_ERR(svn_io_check_resolved_path(right_abspath, &right_kind, scratch_pool));

  if (left_kind == svn_node_dir && right_kind == svn_node_dir)
    {
      SVN_ERR(do_dir_diff(left_abspath, right_abspath,
                          left_root_abspath, right_root_abspath,
                          FALSE, FALSE, left_before_right,
                          depth, NULL /* parent_baton */,
                          diff_processor, ctx, scratch_pool));
    }
  else if (left_kind == svn_node_file && right_kind == svn_node_file)
    {
      SVN_ERR(do_file_diff(left_abspath, right_abspath,
                           left_root_abspath, right_root_abspath,
                           FALSE, FALSE,
                           NULL /* parent_baton */,
                           diff_processor, ctx, scratch_pool));
    }
  else if (left_kind == svn_node_file || left_kind == svn_node_dir
           || right_kind == svn_node_file || right_kind == svn_node_dir)
    {
      /* The root is added/deleted/replaced. Report delete and/or add. */
      if (left_before_right)
        {
          if (left_kind == svn_node_file)
            SVN_ERR(do_file_diff(left_abspath, right_abspath,
                                 left_root_abspath, right_root_abspath,
                                 TRUE, FALSE, NULL /* parent_baton */,
                                 diff_processor, ctx, scratch_pool));
          else if (left_kind == svn_node_dir)
            SVN_ERR(do_dir_diff(left_abspath, right_abspath,
                                left_root_abspath, right_root_abspath,
                                TRUE, FALSE, left_before_right,
                                depth, NULL /* parent_baton */,
                                diff_processor, ctx, scratch_pool));
        }

      if (right_kind == svn_node_file)
        SVN_ERR(do_file_diff(left_abspath, right_abspath,
                             left_root_abspath, right_root_abspath,
                             FALSE, TRUE, NULL /* parent_baton */,
                             diff_processor, ctx, scratch_pool));
      else if (right_kind == svn_node_dir)
        SVN_ERR(do_dir_diff(left_abspath, right_abspath,
                            left_root_abspath, right_root_abspath,
                            FALSE, TRUE, left_before_right,
                            depth, NULL /* parent_baton */,
                            diff_processor, ctx, scratch_pool));

      if (! left_before_right)
        {
          if (left_kind == svn_node_file)
            SVN_ERR(do_file_diff(left_abspath, right_abspath,
                                 left_root_abspath, right_root_abspath,
                                 TRUE, FALSE, NULL /* parent_baton */,
                                 diff_processor, ctx, scratch_pool));
          else if (left_kind == svn_node_dir)
            SVN_ERR(do_dir_diff(left_abspath, right_abspath,
                                left_root_abspath, right_root_abspath,
                                TRUE, FALSE, left_before_right,
                                depth, NULL /* parent_baton */,
                                diff_processor, ctx, scratch_pool));
        }
    }
  else
    return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
                             _("'%s' is not a file or directory"),
                             svn_dirent_local_style(
                                    (left_kind == svn_node_none)
                                        ? left_abspath
                                        : right_abspath,
                                    scratch_pool));

  return SVN_NO_ERROR;
}
