/*
 * conflict-callbacks.c: conflict resolution callbacks specific to the
 * commandline client.
 *
 * ====================================================================
 *    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 <apr_xlate.h>  /* for APR_LOCALE_CHARSET */

#define APR_WANT_STRFUNC
#include <apr_want.h>

#include "svn_hash.h"
#include "svn_cmdline.h"
#include "svn_client.h"
#include "svn_dirent_uri.h"
#include "svn_types.h"
#include "svn_pools.h"
#include "svn_sorts.h"
#include "svn_utf.h"

#include "cl.h"
#include "cl-conflicts.h"

#include "private/svn_cmdline_private.h"

#include "svn_private_config.h"

#define ARRAY_LEN(ary) ((sizeof (ary)) / (sizeof ((ary)[0])))
#define MAX_ARRAY_LEN(aryx, aryz)               \
  (ARRAY_LEN((aryx)) > ARRAY_LEN((aryz))        \
   ? ARRAY_LEN((aryx)) : ARRAY_LEN((aryz)))



struct svn_cl__interactive_conflict_baton_t {
  svn_cl__accept_t accept_which;
  apr_hash_t *config;
  const char *editor_cmd;
  svn_boolean_t external_failed;
  svn_cmdline_prompt_baton_t *pb;
  const char *path_prefix;
  svn_boolean_t quit;
  svn_cl__conflict_stats_t *conflict_stats;
  svn_boolean_t printed_summary;
};

svn_error_t *
svn_cl__get_conflict_func_interactive_baton(
  svn_cl__interactive_conflict_baton_t **b,
  svn_cl__accept_t accept_which,
  apr_hash_t *config,
  const char *editor_cmd,
  svn_cl__conflict_stats_t *conflict_stats,
  svn_cancel_func_t cancel_func,
  void *cancel_baton,
  apr_pool_t *result_pool)
{
  svn_cmdline_prompt_baton_t *pb = apr_palloc(result_pool, sizeof(*pb));
  pb->cancel_func = cancel_func;
  pb->cancel_baton = cancel_baton;

  *b = apr_palloc(result_pool, sizeof(**b));
  (*b)->accept_which = accept_which;
  (*b)->config = config;
  (*b)->editor_cmd = editor_cmd;
  (*b)->external_failed = FALSE;
  (*b)->pb = pb;
  SVN_ERR(svn_dirent_get_absolute(&(*b)->path_prefix, "", result_pool));
  (*b)->quit = FALSE;
  (*b)->conflict_stats = conflict_stats;
  (*b)->printed_summary = FALSE;

  return SVN_NO_ERROR;
}

svn_cl__accept_t
svn_cl__accept_from_word(const char *word)
{
  /* Shorthand options are consistent with  svn_cl__conflict_handler(). */
  if (strcmp(word, SVN_CL__ACCEPT_POSTPONE) == 0
      || strcmp(word, "p") == 0 || strcmp(word, ":-P") == 0)
    return svn_cl__accept_postpone;
  if (strcmp(word, SVN_CL__ACCEPT_BASE) == 0)
    /* ### shorthand? */
    return svn_cl__accept_base;
  if (strcmp(word, SVN_CL__ACCEPT_WORKING) == 0)
    /* ### shorthand? */
    return svn_cl__accept_working;
  if (strcmp(word, SVN_CL__ACCEPT_MINE_CONFLICT) == 0
      || strcmp(word, "mc") == 0 || strcmp(word, "X-)") == 0)
    return svn_cl__accept_mine_conflict;
  if (strcmp(word, SVN_CL__ACCEPT_THEIRS_CONFLICT) == 0
      || strcmp(word, "tc") == 0 || strcmp(word, "X-(") == 0)
    return svn_cl__accept_theirs_conflict;
  if (strcmp(word, SVN_CL__ACCEPT_MINE_FULL) == 0
      || strcmp(word, "mf") == 0 || strcmp(word, ":-)") == 0)
    return svn_cl__accept_mine_full;
  if (strcmp(word, SVN_CL__ACCEPT_THEIRS_FULL) == 0
      || strcmp(word, "tf") == 0 || strcmp(word, ":-(") == 0)
    return svn_cl__accept_theirs_full;
  if (strcmp(word, SVN_CL__ACCEPT_EDIT) == 0
      || strcmp(word, "e") == 0 || strcmp(word, ":-E") == 0)
    return svn_cl__accept_edit;
  if (strcmp(word, SVN_CL__ACCEPT_LAUNCH) == 0
      || strcmp(word, "l") == 0 || strcmp(word, ":-l") == 0)
    return svn_cl__accept_launch;
  /* word is an invalid action. */
  return svn_cl__accept_invalid;
}


/* Print on stdout a diff that shows incoming conflicting changes
 * corresponding to the conflict described in DESC. */
static svn_error_t *
show_diff(const svn_wc_conflict_description2_t *desc,
          const char *path_prefix,
          svn_cancel_func_t cancel_func,
          void *cancel_baton,
          apr_pool_t *pool)
{
  const char *path1, *path2;
  const char *label1, *label2;
  svn_diff_t *diff;
  svn_stream_t *output;
  svn_diff_file_options_t *options;

  if (desc->merged_file)
    {
      /* For conflicts recorded by the 'merge' operation, show a diff between
       * 'mine' (the working version of the file as it appeared before the
       * 'merge' operation was run) and 'merged' (the version of the file
       * as it appears after the merge operation).
       *
       * For conflicts recorded by the 'update' and 'switch' operations,
       * show a diff beween 'theirs' (the new pristine version of the
       * file) and 'merged' (the version of the file as it appears with
       * local changes merged with the new pristine version).
       *
       * This way, the diff is always minimal and clearly identifies changes
       * brought into the working copy by the update/switch/merge operation. */
      if (desc->operation == svn_wc_operation_merge)
        {
          path1 = desc->my_abspath;
          label1 = _("MINE");
        }
      else
        {
          path1 = desc->their_abspath;
          label1 = _("THEIRS");
        }
      path2 = desc->merged_file;
      label2 = _("MERGED");
    }
  else
    {
      /* There's no merged file, but we can show the
         difference between mine and theirs. */
      path1 = desc->their_abspath;
      label1 = _("THEIRS");
      path2 = desc->my_abspath;
      label2 = _("MINE");
    }

  label1 = apr_psprintf(pool, "%s\t- %s",
                        svn_cl__local_style_skip_ancestor(
                          path_prefix, path1, pool), label1);
  label2 = apr_psprintf(pool, "%s\t- %s",
                        svn_cl__local_style_skip_ancestor(
                          path_prefix, path2, pool), label2);

  options = svn_diff_file_options_create(pool);
  options->ignore_eol_style = TRUE;
  SVN_ERR(svn_stream_for_stdout(&output, pool));
  SVN_ERR(svn_diff_file_diff_2(&diff, path1, path2,
                               options, pool));
  return svn_diff_file_output_unified4(output, diff,
                                       path1, path2,
                                       label1, label2,
                                       APR_LOCALE_CHARSET,
                                       NULL,
                                       options->show_c_function,
                                       options->context_size,
                                       cancel_func, cancel_baton,
                                       pool);
}


/* Print on stdout just the conflict hunks of a diff among the 'base', 'their'
 * and 'my' files of DESC. */
static svn_error_t *
show_conflicts(const svn_wc_conflict_description2_t *desc,
               svn_cancel_func_t cancel_func,
               void *cancel_baton,
               apr_pool_t *pool)
{
  svn_diff_t *diff;
  svn_stream_t *output;
  svn_diff_file_options_t *options;

  options = svn_diff_file_options_create(pool);
  options->ignore_eol_style = TRUE;
  SVN_ERR(svn_stream_for_stdout(&output, pool));
  SVN_ERR(svn_diff_file_diff3_2(&diff,
                                desc->base_abspath,
                                desc->my_abspath,
                                desc->their_abspath,
                                options, pool));
  /* ### Consider putting the markers/labels from
     ### svn_wc__merge_internal in the conflict description. */
  return svn_diff_file_output_merge3(output, diff,
                                     desc->base_abspath,
                                     desc->my_abspath,
                                     desc->their_abspath,
                                     _("||||||| ORIGINAL"),
                                     _("<<<<<<< MINE (select with 'mc')"),
                                     _(">>>>>>> THEIRS (select with 'tc')"),
                                     "=======",
                                     svn_diff_conflict_display_only_conflicts,
                                     cancel_func,
                                     cancel_baton,
                                     pool);
}

/* Perform a 3-way merge of the conflicting values of a property,
 * and write the result to the OUTPUT stream.
 *
 * If MERGED_ABSPATH is non-NULL, use it as 'my' version instead of
 * DESC->MY_ABSPATH.
 *
 * Assume the values are printable UTF-8 text.
 */
static svn_error_t *
merge_prop_conflict(svn_stream_t *output,
                    const svn_wc_conflict_description2_t *desc,
                    const char *merged_abspath,
                    svn_cancel_func_t cancel_func,
                    void *cancel_baton,
                    apr_pool_t *pool)
{
  const char *base_abspath = desc->base_abspath;
  const char *my_abspath = desc->my_abspath;
  const char *their_abspath = desc->their_abspath;
  svn_diff_file_options_t *options = svn_diff_file_options_create(pool);
  svn_diff_t *diff;

  /* If any of the property values is missing, use an empty file instead
   * for the purpose of showing a diff. */
  if (! base_abspath || ! my_abspath || ! their_abspath)
    {
      const char *empty_file;

      SVN_ERR(svn_io_open_unique_file3(NULL, &empty_file,
                                       NULL, svn_io_file_del_on_pool_cleanup,
                                       pool, pool));
      if (! base_abspath)
        base_abspath = empty_file;
      if (! my_abspath)
        my_abspath = empty_file;
      if (! their_abspath)
        their_abspath = empty_file;
    }

  options->ignore_eol_style = TRUE;
  SVN_ERR(svn_diff_file_diff3_2(&diff,
                                base_abspath,
                                merged_abspath ? merged_abspath : my_abspath,
                                their_abspath,
                                options, pool));
  SVN_ERR(svn_diff_file_output_merge3(output, diff,
                                      base_abspath,
                                      merged_abspath ? merged_abspath
                                                     : my_abspath,
                                      their_abspath,
                                      _("||||||| ORIGINAL"),
                                      _("<<<<<<< MINE"),
                                      _(">>>>>>> THEIRS"),
                                      "=======",
                                      svn_diff_conflict_display_modified_original_latest,
                                      cancel_func,
                                      cancel_baton,
                                      pool));

  return SVN_NO_ERROR;
}

/* Display the conflicting values of a property as a 3-way diff.
 *
 * If MERGED_ABSPATH is non-NULL, show it as 'my' version instead of
 * DESC->MY_ABSPATH.
 *
 * Assume the values are printable UTF-8 text.
 */
static svn_error_t *
show_prop_conflict(const svn_wc_conflict_description2_t *desc,
                   const char *merged_abspath,
                   svn_cancel_func_t cancel_func,
                   void *cancel_baton,
                   apr_pool_t *pool)
{
  svn_stream_t *output;

  SVN_ERR(svn_stream_for_stdout(&output, pool));
  SVN_ERR(merge_prop_conflict(output, desc, merged_abspath,
                              cancel_func, cancel_baton, pool));

  return SVN_NO_ERROR;
}

/* Run an external editor, passing it the MERGED_FILE, or, if the
 * 'merged' file is null, return an error. The tool to use is determined by
 * B->editor_cmd, B->config and environment variables; see
 * svn_cl__edit_file_externally() for details.
 *
 * If the tool runs, set *PERFORMED_EDIT to true; if a tool is not
 * configured or cannot run, do not touch *PERFORMED_EDIT, report the error
 * on stderr, and return SVN_NO_ERROR; if any other error is encountered,
 * return that error. */
static svn_error_t *
open_editor(svn_boolean_t *performed_edit,
            const char *merged_file,
            svn_cl__interactive_conflict_baton_t *b,
            apr_pool_t *pool)
{
  svn_error_t *err;

  if (merged_file)
    {
      err = svn_cmdline__edit_file_externally(merged_file, b->editor_cmd,
                                              b->config, pool);
      if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_EDITOR ||
                  err->apr_err == SVN_ERR_EXTERNAL_PROGRAM))
        {
          char buf[1024];
          const char *message;

          message = svn_err_best_message(err, buf, sizeof(buf));
          SVN_ERR(svn_cmdline_fprintf(stderr, pool, "%s\n", message));
          svn_error_clear(err);
        }
      else if (err)
        return svn_error_trace(err);
      else
        *performed_edit = TRUE;
    }
  else
    SVN_ERR(svn_cmdline_fprintf(stderr, pool,
                                _("Invalid option; there's no "
                                  "merged version to edit.\n\n")));

  return SVN_NO_ERROR;
}

/* Run an external editor, passing it the 'merged' property in DESC.
 * The tool to use is determined by B->editor_cmd, B->config and
 * environment variables; see svn_cl__edit_file_externally() for details. */
static svn_error_t *
edit_prop_conflict(const char **merged_file_path,
                   const svn_wc_conflict_description2_t *desc,
                   svn_cl__interactive_conflict_baton_t *b,
                   apr_pool_t *result_pool,
                   apr_pool_t *scratch_pool)
{
  apr_file_t *file;
  const char *file_path;
  svn_boolean_t performed_edit = FALSE;
  svn_stream_t *merged_prop;

  SVN_ERR(svn_io_open_unique_file3(&file, &file_path, NULL,
                                   svn_io_file_del_on_pool_cleanup,
                                   result_pool, scratch_pool));
  merged_prop = svn_stream_from_aprfile2(file, TRUE /* disown */,
                                         scratch_pool);
  SVN_ERR(merge_prop_conflict(merged_prop, desc, NULL,
                              b->pb->cancel_func,
                              b->pb->cancel_baton,
                              scratch_pool));
  SVN_ERR(svn_stream_close(merged_prop));
  SVN_ERR(svn_io_file_flush(file, scratch_pool));
  SVN_ERR(open_editor(&performed_edit, file_path, b, scratch_pool));
  *merged_file_path = (performed_edit ? file_path : NULL);

  return SVN_NO_ERROR;
}

/* Maximum line length for the prompt string. */
#define MAX_PROMPT_WIDTH 70

/* Description of a resolver option */
typedef struct resolver_option_t
{
  const char *code;        /* one or two characters */
  const char *short_desc;  /* label in prompt (localized) */
  const char *long_desc;   /* longer description (localized) */
  svn_wc_conflict_choice_t choice;
                           /* or ..._undefined if not a simple choice */
} resolver_option_t;

/* Resolver options for a text conflict */
/* (opt->code == "" causes a blank line break in help_string()) */
static const resolver_option_t text_conflict_options[] =
{
  /* Translators: keep long_desc below 70 characters (wrap with a left
     margin of 9 spaces if needed); don't translate the words within square
     brackets. */
  { "e",  N_("edit file"),        N_("change merged file in an editor"
                                     "  [edit]"),
                                  svn_wc_conflict_choose_undefined },
  { "df", N_("show diff"),        N_("show all changes made to merged file"),
                                  svn_wc_conflict_choose_undefined },
  { "r",  N_("mark resolved"),   N_("accept merged version of file  [working]"),
                                  svn_wc_conflict_choose_merged },
  { "",   "",                     "", svn_wc_conflict_choose_unspecified },
  { "dc", N_("display conflict"), N_("show all conflicts "
                                     "(ignoring merged version)"),
                                  svn_wc_conflict_choose_undefined },
  { "mc", N_("my side of conflict"), N_("accept my version for all conflicts "
                                        "(same)  [mine-conflict]"),
                                  svn_wc_conflict_choose_mine_conflict },
  { "tc", N_("their side of conflict"), N_("accept their version for all "
                                           "conflicts (same)"
                                           "  [theirs-conflict]"),
                                  svn_wc_conflict_choose_theirs_conflict },
  { "",   "",                     "", svn_wc_conflict_choose_unspecified },
  { "mf", N_("my version"),       N_("accept my version of entire file (even "
                                     "non-conflicts)  [mine-full]"),
                                  svn_wc_conflict_choose_mine_full },
  { "tf", N_("their version"),    N_("accept their version of entire file "
                                     "(same)  [theirs-full]"),
                                  svn_wc_conflict_choose_theirs_full },
  { "",   "",                     "", svn_wc_conflict_choose_unspecified },
  { "m",  N_("merge"),            N_("use merge tool to resolve conflict"),
                                  svn_wc_conflict_choose_undefined },
  { "l",  N_("launch tool"),      N_("launch external merge tool to resolve "
                                     "conflict  [launch]"),
                                  svn_wc_conflict_choose_undefined },
  { "i",  N_("internal merge tool"), N_("use built-in merge tool to "
                                     "resolve conflict"),
                                  svn_wc_conflict_choose_undefined },
  { "p",  N_("postpone"),         N_("mark the conflict to be resolved later"
                                     "  [postpone]"),
                                  svn_wc_conflict_choose_postpone },
  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
                                  svn_wc_conflict_choose_postpone },
  { "s",  N_("show all options"), N_("show this list (also 'h', '?')"),
                                  svn_wc_conflict_choose_undefined },
  { NULL }
};

/* Resolver options for a binary file conflict. */
static const resolver_option_t binary_conflict_options[] =
{
  /* Translators: keep long_desc below 70 characters (wrap with a left
     margin of 9 spaces if needed); don't translate the words within square
     brackets. */
  { "r",  N_("mark resolved"),   N_("accept the working copy version of file "
                                    " [working]"),
                                  svn_wc_conflict_choose_merged },
  { "tf", N_("their version"),    N_("accept the incoming version of file "
                                     " [theirs-full]"),
                                  svn_wc_conflict_choose_theirs_full },
  { "p",  N_("postpone"),         N_("mark the conflict to be resolved later "
                                     " [postpone]"),
                                  svn_wc_conflict_choose_postpone },
  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
                                  svn_wc_conflict_choose_postpone },
  { "s",  N_("show all options"), N_("show this list (also 'h', '?')"),
                                  svn_wc_conflict_choose_undefined },
  { NULL }
};

/* Resolver options for a property conflict */
static const resolver_option_t prop_conflict_options[] =
{
  { "mf", N_("my version"),       N_("accept my version of entire property (even "
                                     "non-conflicts)  [mine-full]"),
                                  svn_wc_conflict_choose_mine_full },
  { "tf", N_("their version"),    N_("accept their version of entire property "
                                     "(same)  [theirs-full]"),
                                  svn_wc_conflict_choose_theirs_full },
  { "dc", N_("display conflict"), N_("show conflicts in this property"),
                                  svn_wc_conflict_choose_undefined },
  { "e",  N_("edit property"),    N_("change merged property value in an editor"
                                     "  [edit]"),
                                  svn_wc_conflict_choose_undefined },
  { "r",  N_("mark resolved"),    N_("accept edited version of property"),
                                  svn_wc_conflict_choose_merged },
  { "p",  N_("postpone"),         N_("mark the conflict to be resolved later"
                                     "  [postpone]"),
                                  svn_wc_conflict_choose_postpone },
  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
                                  svn_wc_conflict_choose_postpone },
  { "h",  N_("help"),             N_("show this help (also '?')"),
                                  svn_wc_conflict_choose_undefined },
  { NULL }
};

/* Resolver options for a tree conflict */
static const resolver_option_t tree_conflict_options[] =
{
  { "r",  N_("mark resolved"),    N_("accept current working copy state"),
                                  svn_wc_conflict_choose_merged },
  { "p",  N_("postpone"),         N_("resolve the conflict later  [postpone]"),
                                  svn_wc_conflict_choose_postpone },
  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
                                  svn_wc_conflict_choose_postpone },
  { "h",  N_("help"),             N_("show this help (also '?')"),
                                  svn_wc_conflict_choose_undefined },
  { NULL }
};

static const resolver_option_t tree_conflict_options_update_moved_away[] =
{
  { "mc", N_("apply update to move destination (recommended)"),
                                  N_("apply incoming update to move destination"
                                     "  [mine-conflict]"),
                                  svn_wc_conflict_choose_mine_conflict },
  { "p",  N_("postpone"),         N_("resolve the conflict later  [postpone]"),
                                  svn_wc_conflict_choose_postpone },
  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
                                  svn_wc_conflict_choose_postpone },
  { "h",  N_("help"),             N_("show this help (also '?')"),
                                  svn_wc_conflict_choose_undefined },
  { NULL }
};

static const resolver_option_t tree_conflict_options_update_edit_deleted_dir[] =
{
  { "mc", N_("prepare for updating moved-away children, if any (recommended)"),
                                  N_("allow updating moved-away children "
                                     "with 'svn resolve' [mine-conflict]"),
                                  svn_wc_conflict_choose_mine_conflict },
  { "p",  N_("postpone"),         N_("resolve the conflict later  [postpone]"),
                                  svn_wc_conflict_choose_postpone },
  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
                                  svn_wc_conflict_choose_postpone },
  { "h",  N_("help"),             N_("show this help (also '?')"),
                                  svn_wc_conflict_choose_undefined },
  { NULL }
};

/* Return a pointer to the option description in OPTIONS matching the
 * one- or two-character OPTION_CODE.  Return NULL if not found. */
static const resolver_option_t *
find_option(const resolver_option_t *options,
            const char *option_code)
{
  const resolver_option_t *opt;

  for (opt = options; opt->code; opt++)
    {
      /* Ignore code "" (blank lines) which is not a valid answer. */
      if (opt->code[0] && strcmp(opt->code, option_code) == 0)
        return opt;
    }
  return NULL;
}

/* Return a prompt string listing the options OPTIONS. If OPTION_CODES is
 * non-null, select only the options whose codes are mentioned in it. */
static const char *
prompt_string(const resolver_option_t *options,
              const char *const *option_codes,
              apr_pool_t *pool)
{
  const char *result = _("Select:");
  int left_margin = svn_utf_cstring_utf8_width(result);
  const char *line_sep = apr_psprintf(pool, "\n%*s", left_margin, "");
  int this_line_len = left_margin;
  svn_boolean_t first = TRUE;

  while (1)
    {
      const resolver_option_t *opt;
      const char *s;
      int slen;

      if (option_codes)
        {
          if (! *option_codes)
            break;
          opt = find_option(options, *option_codes++);
        }
      else
        {
          opt = options++;
          if (! opt->code)
            break;
        }

      if (! first)
        result = apr_pstrcat(pool, result, ",", SVN_VA_NULL);
      s = apr_psprintf(pool, _(" (%s) %s"),
                       opt->code, _(opt->short_desc));
      slen = svn_utf_cstring_utf8_width(s);
      /* Break the line if adding the next option would make it too long */
      if (this_line_len + slen > MAX_PROMPT_WIDTH)
        {
          result = apr_pstrcat(pool, result, line_sep, SVN_VA_NULL);
          this_line_len = left_margin;
        }
      result = apr_pstrcat(pool, result, s, SVN_VA_NULL);
      this_line_len += slen;
      first = FALSE;
    }
  return apr_pstrcat(pool, result, ": ", SVN_VA_NULL);
}

/* Return a help string listing the OPTIONS. */
static const char *
help_string(const resolver_option_t *options,
            apr_pool_t *pool)
{
  const char *result = "";
  const resolver_option_t *opt;

  for (opt = options; opt->code; opt++)
    {
      /* Append a line describing OPT, or a blank line if its code is "". */
      if (opt->code[0])
        {
          const char *s = apr_psprintf(pool, "  (%s)", opt->code);

          result = apr_psprintf(pool, "%s%-6s - %s\n",
                                result, s, _(opt->long_desc));
        }
      else
        {
          result = apr_pstrcat(pool, result, "\n", SVN_VA_NULL);
        }
    }
  result = apr_pstrcat(pool, result,
                       _("Words in square brackets are the corresponding "
                         "--accept option arguments.\n"),
                       SVN_VA_NULL);
  return result;
}

/* Prompt the user with CONFLICT_OPTIONS, restricted to the options listed
 * in OPTIONS_TO_SHOW if that is non-null.  Set *OPT to point to the chosen
 * one of CONFLICT_OPTIONS (not necessarily one of OPTIONS_TO_SHOW), or to
 * NULL if the answer was not one of them.
 *
 * If the answer is the (globally recognized) 'help' option, then display
 * the help (on stderr) and return with *OPT == NULL.
 */
static svn_error_t *
prompt_user(const resolver_option_t **opt,
            const resolver_option_t *conflict_options,
            const char *const *options_to_show,
            void *prompt_baton,
            apr_pool_t *scratch_pool)
{
  const char *prompt
    = prompt_string(conflict_options, options_to_show, scratch_pool);
  const char *answer;

  SVN_ERR(svn_cmdline_prompt_user2(&answer, prompt, prompt_baton, scratch_pool));
  if (strcmp(answer, "h") == 0 || strcmp(answer, "?") == 0)
    {
      SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "\n%s\n",
                                  help_string(conflict_options,
                                              scratch_pool)));
      *opt = NULL;
    }
  else
    {
      *opt = find_option(conflict_options, answer);
      if (! *opt)
        {
          SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
                                      _("Unrecognized option.\n\n")));
        }
    }
  return SVN_NO_ERROR;
}

/* Ask the user what to do about the text conflict described by DESC.
 * Return the answer in RESULT. B is the conflict baton for this
 * conflict resolution session.
 * SCRATCH_POOL is used for temporary allocations. */
static svn_error_t *
handle_text_conflict(svn_wc_conflict_result_t *result,
                     const svn_wc_conflict_description2_t *desc,
                     svn_cl__interactive_conflict_baton_t *b,
                     apr_pool_t *scratch_pool)
{
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
  svn_boolean_t diff_allowed = FALSE;
  /* Have they done something that might have affected the merged
     file (so that we need to save a .edited copy by setting the
     result->save_merge flag)? */
  svn_boolean_t performed_edit = FALSE;
  /* Have they done *something* (edit, look at diff, etc) to
     give them a rational basis for choosing (r)esolved? */
  svn_boolean_t knows_something = FALSE;
  const char *local_relpath;

  SVN_ERR_ASSERT(desc->kind == svn_wc_conflict_kind_text);

  local_relpath = svn_cl__local_style_skip_ancestor(b->path_prefix,
                                                    desc->local_abspath,
                                                    scratch_pool);;

  if (desc->is_binary)
    SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
                                _("Conflict discovered in binary file '%s'.\n"),
                                local_relpath));
  else
    SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
                                _("Conflict discovered in file '%s'.\n"),
                                local_relpath));

  /* ### TODO This whole feature availability check is grossly outdated.
     DIFF_ALLOWED needs either to be redefined or to go away.
   */

  /* Diffing can happen between base and merged, to show conflict
     markers to the user (this is the typical 3-way merge
     scenario), or if no base is available, we can show a diff
     between mine and theirs. */
  if (!desc->is_binary &&
      ((desc->merged_file && desc->base_abspath)
      || (!desc->base_abspath && desc->my_abspath && desc->their_abspath)))
    diff_allowed = TRUE;

  while (TRUE)
    {
      const char *options[1 + MAX_ARRAY_LEN(binary_conflict_options,
                                            text_conflict_options)];

      const resolver_option_t *conflict_options = desc->is_binary
                                                    ? binary_conflict_options
                                                    : text_conflict_options;
      const char **next_option = options;
      const resolver_option_t *opt;

      svn_pool_clear(iterpool);

      *next_option++ = "p";
      if (diff_allowed)
        {
          /* We need one more path for this feature. */
          if (desc->my_abspath)
            *next_option++ = "df";

          *next_option++ = "e";

          /* We need one more path for this feature. */
          if (desc->my_abspath)
            *next_option++ = "m";

          if (knows_something)
            *next_option++ = "r";

          *next_option++ = "mc";
          *next_option++ = "tc";
        }
      else
        {
          if (knows_something || desc->is_binary)
            *next_option++ = "r";

          /* The 'mine-full' option selects the ".mine" file so only offer
           * it if that file exists. It does not exist for binary files,
           * for example (questionable historical behaviour since 1.0). */
          if (desc->my_abspath)
            *next_option++ = "mf";

          *next_option++ = "tf";
        }
      *next_option++ = "s";
      *next_option++ = NULL;

      SVN_ERR(prompt_user(&opt, conflict_options, options, b->pb, iterpool));
      if (! opt)
        continue;

      if (strcmp(opt->code, "q") == 0)
        {
          result->choice = opt->choice;
          b->accept_which = svn_cl__accept_postpone;
          b->quit = TRUE;
          break;
        }
      else if (strcmp(opt->code, "s") == 0)
        {
          SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "\n%s\n",
                                      help_string(conflict_options,
                                                  iterpool)));
        }
      else if (strcmp(opt->code, "dc") == 0)
        {
          if (desc->is_binary)
            {
              SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
                                          _("Invalid option; cannot "
                                            "display conflicts for a "
                                            "binary file.\n\n")));
              continue;
            }
          else if (! (desc->my_abspath && desc->base_abspath &&
                      desc->their_abspath))
            {
              SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
                                          _("Invalid option; original "
                                            "files not available.\n\n")));
              continue;
            }
          SVN_ERR(show_conflicts(desc,
                                 b->pb->cancel_func,
                                 b->pb->cancel_baton,
                                 iterpool));
          knows_something = TRUE;
        }
      else if (strcmp(opt->code, "df") == 0)
        {
          /* Re-check preconditions. */
          if (! diff_allowed || ! desc->my_abspath)
            {
              SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
                             _("Invalid option; there's no "
                                "merged version to diff.\n\n")));
              continue;
            }

          SVN_ERR(show_diff(desc, b->path_prefix,
                            b->pb->cancel_func, b->pb->cancel_baton,
                            iterpool));
          knows_something = TRUE;
        }
      else if (strcmp(opt->code, "e") == 0 || strcmp(opt->code, ":-E") == 0)
        {
          SVN_ERR(open_editor(&performed_edit, desc->merged_file, b, iterpool));
          if (performed_edit)
            knows_something = TRUE;
        }
      else if (strcmp(opt->code, "m") == 0 || strcmp(opt->code, ":-g") == 0 ||
               strcmp(opt->code, "=>-") == 0 || strcmp(opt->code, ":>.") == 0)
        {
          svn_error_t *err;

          /* Re-check preconditions. */
          if (! desc->my_abspath)
            {
              SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
                             _("Invalid option; there's no "
                                "base path to merge.\n\n")));
              continue;
            }

          err = svn_cl__merge_file_externally(desc->base_abspath,
                                              desc->their_abspath,
                                              desc->my_abspath,
                                              desc->merged_file,
                                              desc->local_abspath, b->config,
                                              NULL, iterpool);
          if (err)
            {
              if (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_MERGE_TOOL)
                {
                  svn_boolean_t remains_in_conflict = TRUE;

                  /* Try the internal merge tool. */
                  svn_error_clear(err);
                  SVN_ERR(svn_cl__merge_file(&remains_in_conflict,
                                             desc->base_abspath,
                                             desc->their_abspath,
                                             desc->my_abspath,
                                             desc->merged_file,
                                             desc->local_abspath,
                                             b->path_prefix,
                                             b->editor_cmd,
                                             b->config,
                                             b->pb->cancel_func,
                                             b->pb->cancel_baton,
                                             iterpool));
                  knows_something = !remains_in_conflict;
                }
              else if (err->apr_err == SVN_ERR_EXTERNAL_PROGRAM)
                {
                  char buf[1024];
                  const char *message;

                  message = svn_err_best_message(err, buf, sizeof(buf));
                  SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
                                              "%s\n", message));
                  svn_error_clear(err);
                  continue;
                }
              else
                return svn_error_trace(err);
            }
          else
            {
              /* The external merge tool's exit code was either 0 or 1.
               * The tool may leave the file conflicted by exiting with
               * exit code 1, and we allow the user to mark the conflict
               * resolved in this case. */
              performed_edit = TRUE;
              knows_something = TRUE;
            }
        }
      else if (strcmp(opt->code, "l") == 0 || strcmp(opt->code, ":-l") == 0)
        {
          /* ### This check should be earlier as it's nasty to offer an option
           *     and then when the user chooses it say 'Invalid option'. */
          /* ### 'merged_file' shouldn't be necessary *before* we launch the
           *     resolver: it should be the *result* of doing so. */
          if (desc->base_abspath && desc->their_abspath &&
              desc->my_abspath && desc->merged_file)
            {
              svn_error_t *err;
              char buf[1024];
              const char *message;

              err = svn_cl__merge_file_externally(desc->base_abspath,
                                                  desc->their_abspath,
                                                  desc->my_abspath,
                                                  desc->merged_file,
                                                  desc->local_abspath,
                                                  b->config, NULL, iterpool);
              if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_MERGE_TOOL ||
                          err->apr_err == SVN_ERR_EXTERNAL_PROGRAM))
                {
                  message = svn_err_best_message(err, buf, sizeof(buf));
                  SVN_ERR(svn_cmdline_fprintf(stderr, iterpool, "%s\n",
                                              message));
                  svn_error_clear(err);
                }
              else if (err)
                return svn_error_trace(err);
              else
                performed_edit = TRUE;

              if (performed_edit)
                knows_something = TRUE;
            }
          else
            SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
                                        _("Invalid option.\n\n")));
        }
      else if (strcmp(opt->code, "i") == 0)
        {
          svn_boolean_t remains_in_conflict = TRUE;

          SVN_ERR(svn_cl__merge_file(&remains_in_conflict,
                                     desc->base_abspath,
                                     desc->their_abspath,
                                     desc->my_abspath,
                                     desc->merged_file,
                                     desc->local_abspath,
                                     b->path_prefix,
                                     b->editor_cmd,
                                     b->config,
                                     b->pb->cancel_func,
                                     b->pb->cancel_baton,
                                     iterpool));

          if (!remains_in_conflict)
            knows_something = TRUE;
        }
      else if (opt->choice != svn_wc_conflict_choose_undefined)
        {
          if ((opt->choice == svn_wc_conflict_choose_mine_conflict
               || opt->choice == svn_wc_conflict_choose_theirs_conflict)
              && desc->is_binary)
            {
              SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
                                          _("Invalid option; cannot choose "
                                            "based on conflicts in a "
                                            "binary file.\n\n")));
              continue;
            }

          /* We only allow the user accept the merged version of
             the file if they've edited it, or at least looked at
             the diff. */
          if (opt->choice == svn_wc_conflict_choose_merged
              && ! knows_something && diff_allowed)
            {
              SVN_ERR(svn_cmdline_fprintf(
                        stderr, iterpool,
                        _("Invalid option; use diff/edit/merge/launch "
                          "before choosing 'mark resolved'.\n\n")));
              continue;
            }

          result->choice = opt->choice;
          if (performed_edit)
            result->save_merged = TRUE;
          break;
        }
    }
  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

/* Ask the user what to do about the property conflict described by DESC.
 * Return the answer in RESULT. B is the conflict baton for this
 * conflict resolution session.
 * SCRATCH_POOL is used for temporary allocations. */
static svn_error_t *
handle_prop_conflict(svn_wc_conflict_result_t *result,
                     const svn_wc_conflict_description2_t *desc,
                     svn_cl__interactive_conflict_baton_t *b,
                     apr_pool_t *result_pool,
                     apr_pool_t *scratch_pool)
{
  apr_pool_t *iterpool;
  const char *message;
  const char *merged_file_path = NULL;
  svn_boolean_t resolved_allowed = FALSE;

  /* ### Work around a historical bug in the provider: the path to the
   *     conflict description file was put in the 'theirs' field, and
   *     'theirs' was put in the 'merged' field. */
  ((svn_wc_conflict_description2_t *)desc)->their_abspath = desc->merged_file;
  ((svn_wc_conflict_description2_t *)desc)->merged_file = NULL;

  SVN_ERR_ASSERT(desc->kind == svn_wc_conflict_kind_property);

  SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
                              _("Conflict for property '%s' discovered"
                                " on '%s'.\n"),
                              desc->property_name,
                              svn_cl__local_style_skip_ancestor(
                                b->path_prefix, desc->local_abspath,
                                scratch_pool)));

  SVN_ERR(svn_cl__get_human_readable_prop_conflict_description(&message, desc,
                                                               scratch_pool));
  SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n", message));

  iterpool = svn_pool_create(scratch_pool);
  while (TRUE)
    {
      const resolver_option_t *opt;
      const char *options[ARRAY_LEN(prop_conflict_options)];
      const char **next_option = options;

      *next_option++ = "p";
      *next_option++ = "mf";
      *next_option++ = "tf";
      *next_option++ = "dc";
      *next_option++ = "e";
      if (resolved_allowed)
        *next_option++ = "r";
      *next_option++ = "q";
      *next_option++ = "h";
      *next_option++ = NULL;

      svn_pool_clear(iterpool);

      SVN_ERR(prompt_user(&opt, prop_conflict_options, options, b->pb,
                          iterpool));
      if (! opt)
        continue;

      if (strcmp(opt->code, "q") == 0)
        {
          result->choice = opt->choice;
          b->accept_which = svn_cl__accept_postpone;
          b->quit = TRUE;
          break;
        }
      else if (strcmp(opt->code, "dc") == 0)
        {
          SVN_ERR(show_prop_conflict(desc, merged_file_path,
                                     b->pb->cancel_func, b->pb->cancel_baton,
                                     scratch_pool));
        }
      else if (strcmp(opt->code, "e") == 0)
        {
          SVN_ERR(edit_prop_conflict(&merged_file_path, desc, b,
                                     result_pool, scratch_pool));
          resolved_allowed = (merged_file_path != NULL);
        }
      else if (strcmp(opt->code, "r") == 0)
        {
          if (! resolved_allowed)
            {
              SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
                             _("Invalid option; please edit the property "
                               "first.\n\n")));
              continue;
            }

          result->merged_file = merged_file_path;
          result->choice = svn_wc_conflict_choose_merged;
          break;
        }
      else if (opt->choice != svn_wc_conflict_choose_undefined)
        {
          result->choice = opt->choice;
          break;
        }
    }
  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

/* Ask the user what to do about the tree conflict described by DESC.
 * Return the answer in RESULT. B is the conflict baton for this
 * conflict resolution session.
 * SCRATCH_POOL is used for temporary allocations. */
static svn_error_t *
handle_tree_conflict(svn_wc_conflict_result_t *result,
                     const svn_wc_conflict_description2_t *desc,
                     svn_cl__interactive_conflict_baton_t *b,
                     apr_pool_t *scratch_pool)
{
  const char *readable_desc;
  apr_pool_t *iterpool;

  SVN_ERR(svn_cl__get_human_readable_tree_conflict_description(
           &readable_desc, desc, scratch_pool));
  SVN_ERR(svn_cmdline_fprintf(
               stderr, scratch_pool,
               _("Tree conflict on '%s'\n   > %s\n"),
               svn_cl__local_style_skip_ancestor(b->path_prefix,
                                                 desc->local_abspath,
                                                 scratch_pool),
               readable_desc));

  iterpool = svn_pool_create(scratch_pool);
  while (1)
    {
      const resolver_option_t *opt;
      const resolver_option_t *tc_opts;

      svn_pool_clear(iterpool);

      tc_opts = tree_conflict_options;

      if (desc->operation == svn_wc_operation_update ||
          desc->operation == svn_wc_operation_switch)
        {
          if (desc->reason == svn_wc_conflict_reason_moved_away)
            {
              tc_opts = tree_conflict_options_update_moved_away;
            }
          else if (desc->reason == svn_wc_conflict_reason_deleted ||
                   desc->reason == svn_wc_conflict_reason_replaced)
            {
              if (desc->action == svn_wc_conflict_action_edit &&
                  desc->node_kind == svn_node_dir)
                tc_opts = tree_conflict_options_update_edit_deleted_dir;
            }
        }

      SVN_ERR(prompt_user(&opt, tc_opts, NULL, b->pb, iterpool));
      if (! opt)
        continue;

      if (strcmp(opt->code, "q") == 0)
        {
          result->choice = opt->choice;
          b->accept_which = svn_cl__accept_postpone;
          b->quit = TRUE;
          break;
        }
      else if (opt->choice != svn_wc_conflict_choose_undefined)
        {
          result->choice = opt->choice;
          break;
        }
    }
  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

/* The body of svn_cl__conflict_func_interactive(). */
static svn_error_t *
conflict_func_interactive(svn_wc_conflict_result_t **result,
                          const svn_wc_conflict_description2_t *desc,
                          void *baton,
                          apr_pool_t *result_pool,
                          apr_pool_t *scratch_pool)
{
  svn_cl__interactive_conflict_baton_t *b = baton;
  svn_error_t *err;

  /* Start out assuming we're going to postpone the conflict. */
  *result = svn_wc_create_conflict_result(svn_wc_conflict_choose_postpone,
                                          NULL, result_pool);

  switch (b->accept_which)
    {
    case svn_cl__accept_invalid:
    case svn_cl__accept_unspecified:
      /* No (or no valid) --accept option, fall through to prompting. */
      break;
    case svn_cl__accept_postpone:
      (*result)->choice = svn_wc_conflict_choose_postpone;
      return SVN_NO_ERROR;
    case svn_cl__accept_base:
      (*result)->choice = svn_wc_conflict_choose_base;
      return SVN_NO_ERROR;
    case svn_cl__accept_working:
      /* If the caller didn't merge the property values, then I guess
       * 'choose working' means 'choose mine'... */
      if (! desc->merged_file)
        (*result)->merged_file = desc->my_abspath;
      (*result)->choice = svn_wc_conflict_choose_merged;
      return SVN_NO_ERROR;
    case svn_cl__accept_mine_conflict:
      (*result)->choice = svn_wc_conflict_choose_mine_conflict;
      return SVN_NO_ERROR;
    case svn_cl__accept_theirs_conflict:
      (*result)->choice = svn_wc_conflict_choose_theirs_conflict;
      return SVN_NO_ERROR;
    case svn_cl__accept_mine_full:
      (*result)->choice = svn_wc_conflict_choose_mine_full;
      return SVN_NO_ERROR;
    case svn_cl__accept_theirs_full:
      (*result)->choice = svn_wc_conflict_choose_theirs_full;
      return SVN_NO_ERROR;
    case svn_cl__accept_edit:
      if (desc->merged_file)
        {
          if (b->external_failed)
            {
              (*result)->choice = svn_wc_conflict_choose_postpone;
              return SVN_NO_ERROR;
            }

          err = svn_cmdline__edit_file_externally(desc->merged_file,
                                                  b->editor_cmd, b->config,
                                                  scratch_pool);
          if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_EDITOR ||
                      err->apr_err == SVN_ERR_EXTERNAL_PROGRAM))
            {
              char buf[1024];
              const char *message;

              message = svn_err_best_message(err, buf, sizeof(buf));
              SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n",
                                          message));
              svn_error_clear(err);
              b->external_failed = TRUE;
            }
          else if (err)
            return svn_error_trace(err);
          (*result)->choice = svn_wc_conflict_choose_merged;
          return SVN_NO_ERROR;
        }
      /* else, fall through to prompting. */
      break;
    case svn_cl__accept_launch:
      if (desc->base_abspath && desc->their_abspath
          && desc->my_abspath && desc->merged_file)
        {
          svn_boolean_t remains_in_conflict;

          if (b->external_failed)
            {
              (*result)->choice = svn_wc_conflict_choose_postpone;
              return SVN_NO_ERROR;
            }

          err = svn_cl__merge_file_externally(desc->base_abspath,
                                              desc->their_abspath,
                                              desc->my_abspath,
                                              desc->merged_file,
                                              desc->local_abspath,
                                              b->config,
                                              &remains_in_conflict,
                                              scratch_pool);
          if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_MERGE_TOOL ||
                      err->apr_err == SVN_ERR_EXTERNAL_PROGRAM))
            {
              char buf[1024];
              const char *message;

              message = svn_err_best_message(err, buf, sizeof(buf));
              SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n",
                                          message));
              b->external_failed = TRUE;
              return svn_error_trace(err);
            }
          else if (err)
            return svn_error_trace(err);

          if (remains_in_conflict)
            (*result)->choice = svn_wc_conflict_choose_postpone;
          else
            (*result)->choice = svn_wc_conflict_choose_merged;
          return SVN_NO_ERROR;
        }
      /* else, fall through to prompting. */
      break;
    }

  /* Print a summary of conflicts before starting interactive resolution */
  if (! b->printed_summary)
    {
      SVN_ERR(svn_cl__print_conflict_stats(b->conflict_stats, scratch_pool));
      b->printed_summary = TRUE;
    }

  /* We're in interactive mode and either the user gave no --accept
     option or the option did not apply; let's prompt. */

  /* Handle the most common cases, which is either:

     Conflicting edits on a file's text, or
     Conflicting edits on a property.
  */
  if (((desc->kind == svn_wc_conflict_kind_text)
       && (desc->action == svn_wc_conflict_action_edit)
       && (desc->reason == svn_wc_conflict_reason_edited)))
    SVN_ERR(handle_text_conflict(*result, desc, b, scratch_pool));
  else if (desc->kind == svn_wc_conflict_kind_property)
    SVN_ERR(handle_prop_conflict(*result, desc, b, result_pool, scratch_pool));
  else if (desc->kind == svn_wc_conflict_kind_tree)
    SVN_ERR(handle_tree_conflict(*result, desc, b, scratch_pool));

  else /* other types of conflicts -- do nothing about them. */
    {
      (*result)->choice = svn_wc_conflict_choose_postpone;
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_cl__conflict_func_interactive(svn_wc_conflict_result_t **result,
                                  const svn_wc_conflict_description2_t *desc,
                                  void *baton,
                                  apr_pool_t *result_pool,
                                  apr_pool_t *scratch_pool)
{
  svn_cl__interactive_conflict_baton_t *b = baton;

  SVN_ERR(conflict_func_interactive(result, desc, baton,
                                    result_pool, scratch_pool));

  /* If we are resolving a conflict, adjust the summary of conflicts. */
  if ((*result)->choice != svn_wc_conflict_choose_postpone)
    {
      const char *local_path
        = svn_cl__local_style_skip_ancestor(
            b->path_prefix, desc->local_abspath, scratch_pool);

      svn_cl__conflict_stats_resolved(b->conflict_stats, local_path,
                                      desc->kind);
    }
  return SVN_NO_ERROR;
}
