/*
 * diff_memory.c :  routines for doing diffs on in-memory data
 *
 * ====================================================================
 *    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.
 * ====================================================================
 */

#define WANT_MEMFUNC
#define WANT_STRFUNC
#include <apr.h>
#include <apr_want.h>
#include <apr_tables.h>

#include <assert.h>

#include "svn_diff.h"
#include "svn_pools.h"
#include "svn_types.h"
#include "svn_string.h"
#include "svn_utf.h"
#include "diff.h"
#include "svn_private_config.h"
#include "private/svn_adler32.h"
#include "private/svn_diff_private.h"

typedef struct source_tokens_t
{
  /* A token simply is an svn_string_t pointing to
     the data of the in-memory data source, containing
     the raw token text, with length stored in the string */
  apr_array_header_t *tokens;

  /* Next token to be consumed */
  apr_size_t next_token;

  /* The source, containing the in-memory data to be diffed */
  const svn_string_t *source;

  /* The last token ends with a newline character (sequence) */
  svn_boolean_t ends_without_eol;
} source_tokens_t;

typedef struct diff_mem_baton_t
{
  /* The tokens for each of the sources */
  source_tokens_t sources[4];

  /* Normalization buffer; we only ever compare 2 tokens at the same time */
  char *normalization_buf[2];

  /* Options for normalized comparison of the data sources */
  const svn_diff_file_options_t *normalization_options;
} diff_mem_baton_t;


static int
datasource_to_index(svn_diff_datasource_e datasource)
{
  switch (datasource)
    {
    case svn_diff_datasource_original:
      return 0;

    case svn_diff_datasource_modified:
      return 1;

    case svn_diff_datasource_latest:
      return 2;

    case svn_diff_datasource_ancestor:
      return 3;
    }

  return -1;
}


/* Implements svn_diff_fns2_t::datasources_open */
static svn_error_t *
datasources_open(void *baton,
                 apr_off_t *prefix_lines,
                 apr_off_t *suffix_lines,
                 const svn_diff_datasource_e *datasources,
                 apr_size_t datasources_len)
{
  /* Do nothing: everything is already there and initialized to 0 */
  *prefix_lines = 0;
  *suffix_lines = 0;
  return SVN_NO_ERROR;
}


/* Implements svn_diff_fns2_t::datasource_close */
static svn_error_t *
datasource_close(void *baton, svn_diff_datasource_e datasource)
{
  /* Do nothing.  The compare_token function needs previous datasources
   * to stay available until all datasources are processed.
   */

  return SVN_NO_ERROR;
}


/* Implements svn_diff_fns2_t::datasource_get_next_token */
static svn_error_t *
datasource_get_next_token(apr_uint32_t *hash, void **token, void *baton,
                          svn_diff_datasource_e datasource)
{
  diff_mem_baton_t *mem_baton = baton;
  source_tokens_t *src = &(mem_baton->sources[datasource_to_index(datasource)]);

  if ((apr_size_t)src->tokens->nelts > src->next_token)
    {
      /* There are actually tokens to be returned */
      char *buf = mem_baton->normalization_buf[0];
      svn_string_t *tok = (*token)
        = APR_ARRAY_IDX(src->tokens, src->next_token, svn_string_t *);
      apr_off_t len = tok->len;
      svn_diff__normalize_state_t state
        = svn_diff__normalize_state_normal;

      svn_diff__normalize_buffer(&buf, &len, &state, tok->data,
                                 mem_baton->normalization_options);
      *hash = svn__adler32(0, buf, len);
      src->next_token++;
    }
  else
    *token = NULL;

  return SVN_NO_ERROR;
}

/* Implements svn_diff_fns2_t::token_compare */
static svn_error_t *
token_compare(void *baton, void *token1, void *token2, int *result)
{
  /* Implement the same behaviour as diff_file.c:token_compare(),
     but be simpler, because we know we'll have all data in memory */
  diff_mem_baton_t *btn = baton;
  svn_string_t *t1 = token1;
  svn_string_t *t2 = token2;
  char *buf1 = btn->normalization_buf[0];
  char *buf2 = btn->normalization_buf[1];
  apr_off_t len1 = t1->len;
  apr_off_t len2 = t2->len;
  svn_diff__normalize_state_t state = svn_diff__normalize_state_normal;

  svn_diff__normalize_buffer(&buf1, &len1, &state, t1->data,
                             btn->normalization_options);
  state = svn_diff__normalize_state_normal;
  svn_diff__normalize_buffer(&buf2, &len2, &state, t2->data,
                             btn->normalization_options);

  if (len1 != len2)
    *result = (len1 < len2) ? -1 : 1;
  else
    *result = (len1 == 0) ? 0 : memcmp(buf1, buf2, (size_t) len1);

  return SVN_NO_ERROR;
}

/* Implements svn_diff_fns2_t::token_discard */
static void
token_discard(void *baton, void *token)
{
  /* No-op, we have no use for discarded tokens... */
}


/* Implements svn_diff_fns2_t::token_discard_all */
static void
token_discard_all(void *baton)
{
  /* Do nothing.
     Note that in the file case, this function discards all
     tokens allocated, but we're geared toward small in-memory diffs.
     Meaning that there's no special pool to clear.
  */
}


static const svn_diff_fns2_t svn_diff__mem_vtable =
{
  datasources_open,
  datasource_close,
  datasource_get_next_token,
  token_compare,
  token_discard,
  token_discard_all
};

/* Fill SRC with the diff tokens (e.g. lines).

   TEXT is assumed to live long enough for the tokens to
   stay valid during their lifetime: no data is copied,
   instead, svn_string_t's are allocated pointing straight
   into TEXT.
*/
static void
fill_source_tokens(source_tokens_t *src,
                   const svn_string_t *text,
                   apr_pool_t *pool)
{
  const char *curp;
  const char *endp;
  const char *startp;

  src->tokens = apr_array_make(pool, 0, sizeof(svn_string_t *));
  src->next_token = 0;
  src->source = text;

  for (startp = curp = text->data, endp = curp + text->len;
       curp != endp; curp++)
    {
      if (curp != endp && *curp == '\r' && *(curp + 1) == '\n')
        curp++;

      if (*curp == '\r' || *curp == '\n')
        {
          APR_ARRAY_PUSH(src->tokens, svn_string_t *) =
            svn_string_ncreate(startp, curp - startp + 1, pool);

          startp = curp + 1;
        }
    }

  /* If there's anything remaining (ie last line doesn't have a newline) */
  if (startp != endp)
    {
      APR_ARRAY_PUSH(src->tokens, svn_string_t *) =
        svn_string_ncreate(startp, endp - startp, pool);
      src->ends_without_eol = TRUE;
    }
  else
    src->ends_without_eol = FALSE;
}


static void
alloc_normalization_bufs(diff_mem_baton_t *btn,
                         int sources,
                         apr_pool_t *pool)
{
  apr_size_t max_len = 0;
  apr_off_t idx;
  int i;

  for (i = 0; i < sources; i++)
    {
      apr_array_header_t *tokens = btn->sources[i].tokens;
      if (tokens->nelts > 0)
        for (idx = 0; idx < tokens->nelts; idx++)
          {
            apr_size_t token_len
              = APR_ARRAY_IDX(tokens, idx, svn_string_t *)->len;
            max_len = (max_len < token_len) ? token_len : max_len;
          }
    }

  btn->normalization_buf[0] = apr_palloc(pool, max_len);
  btn->normalization_buf[1] = apr_palloc(pool, max_len);
}

svn_error_t *
svn_diff_mem_string_diff(svn_diff_t **diff,
                         const svn_string_t *original,
                         const svn_string_t *modified,
                         const svn_diff_file_options_t *options,
                         apr_pool_t *pool)
{
  diff_mem_baton_t baton;

  fill_source_tokens(&(baton.sources[0]), original, pool);
  fill_source_tokens(&(baton.sources[1]), modified, pool);
  alloc_normalization_bufs(&baton, 2, pool);

  baton.normalization_options = options;

  return svn_diff_diff_2(diff, &baton, &svn_diff__mem_vtable, pool);
}

svn_error_t *
svn_diff_mem_string_diff3(svn_diff_t **diff,
                          const svn_string_t *original,
                          const svn_string_t *modified,
                          const svn_string_t *latest,
                          const svn_diff_file_options_t *options,
                          apr_pool_t *pool)
{
  diff_mem_baton_t baton;

  fill_source_tokens(&(baton.sources[0]), original, pool);
  fill_source_tokens(&(baton.sources[1]), modified, pool);
  fill_source_tokens(&(baton.sources[2]), latest, pool);
  alloc_normalization_bufs(&baton, 3, pool);

  baton.normalization_options = options;

  return svn_diff_diff3_2(diff, &baton, &svn_diff__mem_vtable, pool);
}


svn_error_t *
svn_diff_mem_string_diff4(svn_diff_t **diff,
                          const svn_string_t *original,
                          const svn_string_t *modified,
                          const svn_string_t *latest,
                          const svn_string_t *ancestor,
                          const svn_diff_file_options_t *options,
                          apr_pool_t *pool)
{
  diff_mem_baton_t baton;

  fill_source_tokens(&(baton.sources[0]), original, pool);
  fill_source_tokens(&(baton.sources[1]), modified, pool);
  fill_source_tokens(&(baton.sources[2]), latest, pool);
  fill_source_tokens(&(baton.sources[3]), ancestor, pool);
  alloc_normalization_bufs(&baton, 4, pool);

  baton.normalization_options = options;

  return svn_diff_diff4_2(diff, &baton, &svn_diff__mem_vtable, pool);
}


typedef enum unified_output_e
{
  unified_output_context = 0,
  unified_output_delete,
  unified_output_insert,
  unified_output_skip
} unified_output_e;

/* Baton for generating unified diffs */
typedef struct unified_output_baton_t
{
  svn_stream_t *output_stream;
  const char *header_encoding;
  source_tokens_t sources[2]; /* 0 == original; 1 == modified */
  apr_off_t current_token[2]; /* current token per source */

  /* Cached markers, in header_encoding,
     indexed using unified_output_e */
  const char *prefix_str[3];

  svn_stringbuf_t *hunk;    /* in-progress hunk data */
  apr_off_t hunk_length[2]; /* 0 == original; 1 == modified */
  apr_off_t hunk_start[2];  /* 0 == original; 1 == modified */

  /* The delimiters of the hunk header, '@@' for text hunks and '##' for
   * property hunks. */
  const char *hunk_delimiter;
  /* The string to print after a line that does not end with a newline.
   * It must start with a '\'.  Typically "\ No newline at end of file". */
  const char *no_newline_string;

  /* Pool for allocation of temporary memory in the callbacks
     Should be cleared on entry of each iteration of a callback */
  apr_pool_t *pool;
} output_baton_t;


/* Append tokens (lines) FIRST up to PAST_LAST
   from token-source index TOKENS with change-type TYPE
   to the current hunk.
*/
static svn_error_t *
output_unified_token_range(output_baton_t *btn,
                           int tokens,
                           unified_output_e type,
                           apr_off_t until)
{
  source_tokens_t *source = &btn->sources[tokens];

  if (until > source->tokens->nelts)
    until = source->tokens->nelts;

  if (until <= btn->current_token[tokens])
    return SVN_NO_ERROR;

  /* Do the loop with prefix and token */
  while (TRUE)
    {
      svn_string_t *token =
        APR_ARRAY_IDX(source->tokens, btn->current_token[tokens],
                      svn_string_t *);

      if (type != unified_output_skip)
        {
          svn_stringbuf_appendcstr(btn->hunk, btn->prefix_str[type]);
          svn_stringbuf_appendbytes(btn->hunk, token->data, token->len);
        }

      if (type == unified_output_context)
        {
          btn->hunk_length[0]++;
          btn->hunk_length[1]++;
        }
      else if (type == unified_output_delete)
        btn->hunk_length[0]++;
      else if (type == unified_output_insert)
        btn->hunk_length[1]++;

      /* ### TODO: Add skip processing for -p handling? */

      btn->current_token[tokens]++;
      if (btn->current_token[tokens] == until)
        break;
    }

  if (btn->current_token[tokens] == source->tokens->nelts
      && source->ends_without_eol)
    {
      const char *out_str;

      SVN_ERR(svn_utf_cstring_from_utf8_ex2(
                &out_str, btn->no_newline_string,
                btn->header_encoding, btn->pool));
      svn_stringbuf_appendcstr(btn->hunk, out_str);
    }



  return SVN_NO_ERROR;
}

/* Flush the hunk currently built up in BATON
   into the BATON's output_stream.
   Use the specified HUNK_DELIMITER.
   If HUNK_DELIMITER is NULL, fall back to the default delimiter. */
static svn_error_t *
output_unified_flush_hunk(output_baton_t *baton,
                          const char *hunk_delimiter)
{
  apr_off_t target_token;
  apr_size_t hunk_len;
  apr_off_t old_start;
  apr_off_t new_start;

  if (svn_stringbuf_isempty(baton->hunk))
    return SVN_NO_ERROR;

  svn_pool_clear(baton->pool);

  /* Write the trailing context */
  target_token = baton->hunk_start[0] + baton->hunk_length[0]
                 + SVN_DIFF__UNIFIED_CONTEXT_SIZE;
  SVN_ERR(output_unified_token_range(baton, 0 /*original*/,
                                     unified_output_context,
                                     target_token));
  if (hunk_delimiter == NULL)
    hunk_delimiter = "@@";

  old_start = baton->hunk_start[0];
  new_start = baton->hunk_start[1];

  /* If the file is non-empty, convert the line indexes from
     zero based to one based */
  if (baton->hunk_length[0])
    old_start++;
  if (baton->hunk_length[1])
    new_start++;

  /* Write the hunk header */
  SVN_ERR(svn_diff__unified_write_hunk_header(
            baton->output_stream, baton->header_encoding, hunk_delimiter,
            old_start, baton->hunk_length[0],
            new_start, baton->hunk_length[1],
            NULL /* hunk_extra_context */,
            baton->pool));

  hunk_len = baton->hunk->len;
  SVN_ERR(svn_stream_write(baton->output_stream,
                           baton->hunk->data, &hunk_len));

  /* Prepare for the next hunk */
  baton->hunk_length[0] = 0;
  baton->hunk_length[1] = 0;
  baton->hunk_start[0] = 0;
  baton->hunk_start[1] = 0;
  svn_stringbuf_setempty(baton->hunk);

  return SVN_NO_ERROR;
}

/* Implements svn_diff_output_fns_t::output_diff_modified */
static svn_error_t *
output_unified_diff_modified(void *baton,
                             apr_off_t original_start,
                             apr_off_t original_length,
                             apr_off_t modified_start,
                             apr_off_t modified_length,
                             apr_off_t latest_start,
                             apr_off_t latest_length)
{
  output_baton_t *output_baton = baton;
  apr_off_t context_prefix_length;
  apr_off_t prev_context_end;
  svn_boolean_t init_hunk = FALSE;

  if (original_start > SVN_DIFF__UNIFIED_CONTEXT_SIZE)
    context_prefix_length = SVN_DIFF__UNIFIED_CONTEXT_SIZE;
  else
    context_prefix_length = original_start;

  /* Calculate where the previous hunk will end if we would write it now
     (including the necessary context at the end) */
  if (output_baton->hunk_length[0] > 0 || output_baton->hunk_length[1] > 0)
    {
      prev_context_end = output_baton->hunk_start[0]
                         + output_baton->hunk_length[0]
                         + SVN_DIFF__UNIFIED_CONTEXT_SIZE;
    }
  else
    {
      prev_context_end = -1;

      if (output_baton->hunk_start[0] == 0
          && (original_length > 0 || modified_length > 0))
        init_hunk = TRUE;
    }

  /* If the changed range is far enough from the previous range, flush the current
     hunk. */
  {
    apr_off_t new_hunk_start = (original_start - context_prefix_length);

    if (output_baton->current_token[0] < new_hunk_start
          && prev_context_end <= new_hunk_start)
      {
        SVN_ERR(output_unified_flush_hunk(output_baton,
                                          output_baton->hunk_delimiter));
        init_hunk = TRUE;
      }
    else if (output_baton->hunk_length[0] > 0
             || output_baton->hunk_length[1] > 0)
      {
        /* We extend the current hunk */

        /* Original: Output the context preceding the changed range */
        SVN_ERR(output_unified_token_range(output_baton, 0 /* original */,
                                           unified_output_context,
                                           original_start));
      }
  }

  /* Original: Skip lines until we are at the beginning of the context we want
     to display */
  SVN_ERR(output_unified_token_range(output_baton, 0 /* original */,
                                     unified_output_skip,
                                     original_start - context_prefix_length));

  if (init_hunk)
    {
      SVN_ERR_ASSERT(output_baton->hunk_length[0] == 0
                     && output_baton->hunk_length[1] == 0);

      output_baton->hunk_start[0] = original_start - context_prefix_length;
      output_baton->hunk_start[1] = modified_start - context_prefix_length;
    }

  /* Modified: Skip lines until we are at the start of the changed range */
  SVN_ERR(output_unified_token_range(output_baton, 1 /* modified */,
                                     unified_output_skip,
                                     modified_start));

  /* Original: Output the context preceding the changed range */
  SVN_ERR(output_unified_token_range(output_baton, 0 /* original */,
                                    unified_output_context,
                                    original_start));

  /* Both: Output the changed range */
  SVN_ERR(output_unified_token_range(output_baton, 0 /* original */,
                                     unified_output_delete,
                                     original_start + original_length));
  SVN_ERR(output_unified_token_range(output_baton, 1 /* modified */,
                                     unified_output_insert,
                                     modified_start + modified_length));

  return SVN_NO_ERROR;
}

static const svn_diff_output_fns_t mem_output_unified_vtable =
{
  NULL, /* output_common */
  output_unified_diff_modified,
  NULL, /* output_diff_latest */
  NULL, /* output_diff_common */
  NULL  /* output_conflict */
};


svn_error_t *
svn_diff_mem_string_output_unified2(svn_stream_t *output_stream,
                                    svn_diff_t *diff,
                                    svn_boolean_t with_diff_header,
                                    const char *hunk_delimiter,
                                    const char *original_header,
                                    const char *modified_header,
                                    const char *header_encoding,
                                    const svn_string_t *original,
                                    const svn_string_t *modified,
                                    apr_pool_t *pool)
{

  if (svn_diff_contains_diffs(diff))
    {
      output_baton_t baton;

      memset(&baton, 0, sizeof(baton));
      baton.output_stream = output_stream;
      baton.pool = svn_pool_create(pool);
      baton.header_encoding = header_encoding;
      baton.hunk = svn_stringbuf_create_empty(pool);
      baton.hunk_delimiter = hunk_delimiter;
      baton.no_newline_string
        = (hunk_delimiter == NULL || strcmp(hunk_delimiter, "##") != 0)
          ? APR_EOL_STR SVN_DIFF__NO_NEWLINE_AT_END_OF_FILE APR_EOL_STR
          : APR_EOL_STR SVN_DIFF__NO_NEWLINE_AT_END_OF_PROPERTY APR_EOL_STR;

      SVN_ERR(svn_utf_cstring_from_utf8_ex2
              (&(baton.prefix_str[unified_output_context]), " ",
               header_encoding, pool));
      SVN_ERR(svn_utf_cstring_from_utf8_ex2
              (&(baton.prefix_str[unified_output_delete]), "-",
               header_encoding, pool));
      SVN_ERR(svn_utf_cstring_from_utf8_ex2
              (&(baton.prefix_str[unified_output_insert]), "+",
               header_encoding, pool));

      fill_source_tokens(&baton.sources[0], original, pool);
      fill_source_tokens(&baton.sources[1], modified, pool);

      if (with_diff_header)
        {
          SVN_ERR(svn_diff__unidiff_write_header(
                    output_stream, header_encoding,
                    original_header, modified_header, pool));
        }

      SVN_ERR(svn_diff_output(diff, &baton,
                              &mem_output_unified_vtable));

      SVN_ERR(output_unified_flush_hunk(&baton, hunk_delimiter));

      svn_pool_destroy(baton.pool);
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_diff_mem_string_output_unified(svn_stream_t *output_stream,
                                   svn_diff_t *diff,
                                   const char *original_header,
                                   const char *modified_header,
                                   const char *header_encoding,
                                   const svn_string_t *original,
                                   const svn_string_t *modified,
                                   apr_pool_t *pool)
{
  SVN_ERR(svn_diff_mem_string_output_unified2(output_stream,
                                              diff,
                                              TRUE,
                                              NULL,
                                              original_header,
                                              modified_header,
                                              header_encoding,
                                              original,
                                              modified,
                                              pool));
  return SVN_NO_ERROR;
}



/* diff3 merge output */

/* A stream to remember *leading* context.  Note that this stream does
   *not* copy the data that it is remembering; it just saves
   *pointers! */
typedef struct context_saver_t {
  svn_stream_t *stream;
  const char *data[SVN_DIFF__UNIFIED_CONTEXT_SIZE];
  apr_size_t len[SVN_DIFF__UNIFIED_CONTEXT_SIZE];
  apr_size_t next_slot;
  apr_size_t total_written;
} context_saver_t;


static svn_error_t *
context_saver_stream_write(void *baton,
                           const char *data,
                           apr_size_t *len)
{
  context_saver_t *cs = baton;
  cs->data[cs->next_slot] = data;
  cs->len[cs->next_slot] = *len;
  cs->next_slot = (cs->next_slot + 1) % SVN_DIFF__UNIFIED_CONTEXT_SIZE;
  cs->total_written++;
  return SVN_NO_ERROR;
}


typedef struct merge_output_baton_t
{
  svn_stream_t *output_stream;

  /* Tokenized source text */
  source_tokens_t sources[3];
  apr_off_t next_token[3];

  /* Markers for marking conflicted sections */
  const char *markers[4]; /* 0 = original, 1 = modified,
                             2 = separator, 3 = latest (end) */
  const char *marker_eol;

  svn_diff_conflict_display_style_t conflict_style;

  /* The rest of the fields are for
     svn_diff_conflict_display_only_conflicts only.  Note that for
     these batons, OUTPUT_STREAM is either CONTEXT_SAVER->STREAM or
     (soon after a conflict) a "trailing context stream", never the
     actual output stream.*/
  /* The actual output stream. */
  svn_stream_t *real_output_stream;
  context_saver_t *context_saver;
  /* Used to allocate context_saver and trailing context streams, and
     for some printfs. */
  apr_pool_t *pool;
} merge_output_baton_t;


static svn_error_t *
flush_context_saver(context_saver_t *cs,
                    svn_stream_t *output_stream)
{
  int i;
  for (i = 0; i < SVN_DIFF__UNIFIED_CONTEXT_SIZE; i++)
    {
      apr_size_t slot = (i + cs->next_slot) % SVN_DIFF__UNIFIED_CONTEXT_SIZE;
      if (cs->data[slot])
        {
          apr_size_t len = cs->len[slot];
          SVN_ERR(svn_stream_write(output_stream, cs->data[slot], &len));
        }
    }
  return SVN_NO_ERROR;
}


static void
make_context_saver(merge_output_baton_t *mob)
{
  context_saver_t *cs;

  svn_pool_clear(mob->pool);
  cs = apr_pcalloc(mob->pool, sizeof(*cs));
  cs->stream = svn_stream_empty(mob->pool);
  svn_stream_set_baton(cs->stream, cs);
  svn_stream_set_write(cs->stream, context_saver_stream_write);
  mob->context_saver = cs;
  mob->output_stream = cs->stream;
}


/* A stream which prints SVN_DIFF__UNIFIED_CONTEXT_SIZE lines to
   BATON->REAL_OUTPUT_STREAM, and then changes BATON->OUTPUT_STREAM to
   a context_saver; used for *trailing* context. */

struct trailing_context_printer {
  apr_size_t lines_to_print;
  merge_output_baton_t *mob;
};


static svn_error_t *
trailing_context_printer_write(void *baton,
                               const char *data,
                               apr_size_t *len)
{
  struct trailing_context_printer *tcp = baton;
  SVN_ERR_ASSERT(tcp->lines_to_print > 0);
  SVN_ERR(svn_stream_write(tcp->mob->real_output_stream, data, len));
  tcp->lines_to_print--;
  if (tcp->lines_to_print == 0)
    make_context_saver(tcp->mob);
  return SVN_NO_ERROR;
}


static void
make_trailing_context_printer(merge_output_baton_t *btn)
{
  struct trailing_context_printer *tcp;
  svn_stream_t *s;

  svn_pool_clear(btn->pool);

  tcp = apr_pcalloc(btn->pool, sizeof(*tcp));
  tcp->lines_to_print = SVN_DIFF__UNIFIED_CONTEXT_SIZE;
  tcp->mob = btn;
  s = svn_stream_empty(btn->pool);
  svn_stream_set_baton(s, tcp);
  svn_stream_set_write(s, trailing_context_printer_write);
  btn->output_stream = s;
}


static svn_error_t *
output_merge_token_range(apr_size_t *lines_printed_p,
                         merge_output_baton_t *btn,
                         int idx, apr_off_t first,
                         apr_off_t length)
{
  apr_array_header_t *tokens = btn->sources[idx].tokens;
  apr_size_t lines_printed = 0;

  for (; length > 0 && first < tokens->nelts; length--, first++)
    {
      svn_string_t *token = APR_ARRAY_IDX(tokens, first, svn_string_t *);
      apr_size_t len = token->len;

      /* Note that the trailing context printer assumes that
         svn_stream_write is called exactly once per line. */
      SVN_ERR(svn_stream_write(btn->output_stream, token->data, &len));
      lines_printed++;
    }

  if (lines_printed_p)
    *lines_printed_p = lines_printed;

  return SVN_NO_ERROR;
}

static svn_error_t *
output_marker_eol(merge_output_baton_t *btn)
{
  return svn_stream_puts(btn->output_stream, btn->marker_eol);
}

static svn_error_t *
output_merge_marker(merge_output_baton_t *btn, int idx)
{
  SVN_ERR(svn_stream_puts(btn->output_stream, btn->markers[idx]));
  return output_marker_eol(btn);
}

static svn_error_t *
output_common_modified(void *baton,
                       apr_off_t original_start, apr_off_t original_length,
                       apr_off_t modified_start, apr_off_t modified_length,
                       apr_off_t latest_start, apr_off_t latest_length)
{
  return output_merge_token_range(NULL, baton, 1/*modified*/,
                                  modified_start, modified_length);
}

static svn_error_t *
output_latest(void *baton,
              apr_off_t original_start, apr_off_t original_length,
              apr_off_t modified_start, apr_off_t modified_length,
              apr_off_t latest_start, apr_off_t latest_length)
{
  return output_merge_token_range(NULL, baton, 2/*latest*/,
                                  latest_start, latest_length);
}

static svn_error_t *
output_conflict(void *baton,
                apr_off_t original_start, apr_off_t original_length,
                apr_off_t modified_start, apr_off_t modified_length,
                apr_off_t latest_start, apr_off_t latest_length,
                svn_diff_t *diff);

static const svn_diff_output_fns_t merge_output_vtable =
{
  output_common_modified, /* common */
  output_common_modified, /* modified */
  output_latest,
  output_common_modified, /* output_diff_common */
  output_conflict
};

static svn_error_t *
output_conflict(void *baton,
                apr_off_t original_start, apr_off_t original_length,
                apr_off_t modified_start, apr_off_t modified_length,
                apr_off_t latest_start, apr_off_t latest_length,
                svn_diff_t *diff)
{
  merge_output_baton_t *btn = baton;

  svn_diff_conflict_display_style_t style = btn->conflict_style;

  if (style == svn_diff_conflict_display_resolved_modified_latest)
    {
      if (diff)
        return svn_diff_output(diff, baton, &merge_output_vtable);
      else
        style = svn_diff_conflict_display_modified_latest;
    }

  if (style == svn_diff_conflict_display_modified_latest ||
      style == svn_diff_conflict_display_modified_original_latest)
    {
      SVN_ERR(output_merge_marker(btn, 1/*modified*/));
      SVN_ERR(output_merge_token_range(NULL, btn, 1/*modified*/,
                                       modified_start, modified_length));

      if (style == svn_diff_conflict_display_modified_original_latest)
        {
          SVN_ERR(output_merge_marker(btn, 0/*original*/));
          SVN_ERR(output_merge_token_range(NULL, btn, 0/*original*/,
                                           original_start, original_length));
        }

      SVN_ERR(output_merge_marker(btn, 2/*separator*/));
      SVN_ERR(output_merge_token_range(NULL, btn, 2/*latest*/,
                                       latest_start, latest_length));
      SVN_ERR(output_merge_marker(btn, 3/*latest (end)*/));
    }
  else if (style == svn_diff_conflict_display_modified)
      SVN_ERR(output_merge_token_range(NULL, btn, 1/*modified*/,
                                       modified_start, modified_length));
  else if (style == svn_diff_conflict_display_latest)
      SVN_ERR(output_merge_token_range(NULL, btn, 2/*latest*/,
                                       latest_start, latest_length));
  else /* unknown style */
    SVN_ERR_MALFUNCTION();

  return SVN_NO_ERROR;
}


static svn_error_t *
output_conflict_with_context(void *baton,
                             apr_off_t original_start,
                             apr_off_t original_length,
                             apr_off_t modified_start,
                             apr_off_t modified_length,
                             apr_off_t latest_start,
                             apr_off_t latest_length,
                             svn_diff_t *diff)
{
  merge_output_baton_t *btn = baton;

  /* Are we currently saving starting context (as opposed to printing
     trailing context)?  If so, flush it. */
  if (btn->output_stream == btn->context_saver->stream)
    {
      if (btn->context_saver->total_written > SVN_DIFF__UNIFIED_CONTEXT_SIZE)
        SVN_ERR(svn_stream_puts(btn->real_output_stream, "@@\n"));
      SVN_ERR(flush_context_saver(btn->context_saver, btn->real_output_stream));
    }

  /* Print to the real output stream. */
  btn->output_stream = btn->real_output_stream;

  /* Output the conflict itself. */
  SVN_ERR(svn_stream_printf(btn->output_stream, btn->pool,
                            (modified_length == 1
                             ? "%s (%" APR_OFF_T_FMT ")"
                             : "%s (%" APR_OFF_T_FMT ",%" APR_OFF_T_FMT ")"),
                            btn->markers[1],
                            modified_start + 1, modified_length));
  SVN_ERR(output_marker_eol(btn));
  SVN_ERR(output_merge_token_range(NULL, btn, 1/*modified*/,
                                   modified_start, modified_length));

  SVN_ERR(svn_stream_printf(btn->output_stream, btn->pool,
                            (original_length == 1
                             ? "%s (%" APR_OFF_T_FMT ")"
                             : "%s (%" APR_OFF_T_FMT ",%" APR_OFF_T_FMT ")"),
                            btn->markers[0],
                            original_start + 1, original_length));
  SVN_ERR(output_marker_eol(btn));
  SVN_ERR(output_merge_token_range(NULL, btn, 0/*original*/,
                                   original_start, original_length));

  SVN_ERR(output_merge_marker(btn, 2/*separator*/));
  SVN_ERR(output_merge_token_range(NULL, btn, 2/*latest*/,
                                   latest_start, latest_length));
  SVN_ERR(svn_stream_printf(btn->output_stream, btn->pool,
                            (latest_length == 1
                             ? "%s (%" APR_OFF_T_FMT ")"
                             : "%s (%" APR_OFF_T_FMT ",%" APR_OFF_T_FMT ")"),
                            btn->markers[3],
                            latest_start + 1, latest_length));
  SVN_ERR(output_marker_eol(btn));

  /* Go into print-trailing-context mode instead. */
  make_trailing_context_printer(btn);

  return SVN_NO_ERROR;
}


static const svn_diff_output_fns_t merge_only_conflicts_output_vtable =
{
  output_common_modified,
  output_common_modified,
  output_latest,
  output_common_modified,
  output_conflict_with_context
};


/* TOKEN is the first token in the modified file.
   Return its line-ending, if any. */
static const char *
detect_eol(svn_string_t *token)
{
  const char *curp;

  if (token->len == 0)
    return NULL;

  curp = token->data + token->len - 1;
  if (*curp == '\r')
    return "\r";
  else if (*curp != '\n')
    return NULL;
  else
    {
      if (token->len == 1
          || *(--curp) != '\r')
        return "\n";
      else
        return "\r\n";
    }
}

svn_error_t *
svn_diff_mem_string_output_merge2(svn_stream_t *output_stream,
                                  svn_diff_t *diff,
                                  const svn_string_t *original,
                                  const svn_string_t *modified,
                                  const svn_string_t *latest,
                                  const char *conflict_original,
                                  const char *conflict_modified,
                                  const char *conflict_latest,
                                  const char *conflict_separator,
                                  svn_diff_conflict_display_style_t style,
                                  apr_pool_t *pool)
{
  merge_output_baton_t btn;
  const char *eol;
  svn_boolean_t conflicts_only =
    (style == svn_diff_conflict_display_only_conflicts);
  const svn_diff_output_fns_t *vtable = conflicts_only
     ? &merge_only_conflicts_output_vtable : &merge_output_vtable;

  memset(&btn, 0, sizeof(btn));

  if (conflicts_only)
    {
      btn.pool = svn_pool_create(pool);
      make_context_saver(&btn);
      btn.real_output_stream = output_stream;
    }
  else
    btn.output_stream = output_stream;

  fill_source_tokens(&(btn.sources[0]), original, pool);
  fill_source_tokens(&(btn.sources[1]), modified, pool);
  fill_source_tokens(&(btn.sources[2]), latest, pool);

  btn.conflict_style = style;

  if (btn.sources[1].tokens->nelts > 0)
    {
      eol = detect_eol(APR_ARRAY_IDX(btn.sources[1].tokens, 0, svn_string_t *));
      if (!eol)
        eol = APR_EOL_STR;  /* use the platform default */
    }
  else
    eol = APR_EOL_STR;  /* use the platform default */

  btn.marker_eol = eol;

  SVN_ERR(svn_utf_cstring_from_utf8(&btn.markers[1],
                                    conflict_modified
                                    ? conflict_modified
                                    : "<<<<<<< (modified)",
                                    pool));
  SVN_ERR(svn_utf_cstring_from_utf8(&btn.markers[0],
                                    conflict_original
                                    ? conflict_original
                                    : "||||||| (original)",
                                    pool));
  SVN_ERR(svn_utf_cstring_from_utf8(&btn.markers[2],
                                    conflict_separator
                                    ? conflict_separator
                                    : "=======",
                                    pool));
  SVN_ERR(svn_utf_cstring_from_utf8(&btn.markers[3],
                                    conflict_latest
                                    ? conflict_latest
                                    : ">>>>>>> (latest)",
                                    pool));

  SVN_ERR(svn_diff_output(diff, &btn, vtable));
  if (conflicts_only)
    svn_pool_destroy(btn.pool);

  return SVN_NO_ERROR;
}

svn_error_t *
svn_diff_mem_string_output_merge(svn_stream_t *output_stream,
                                 svn_diff_t *diff,
                                 const svn_string_t *original,
                                 const svn_string_t *modified,
                                 const svn_string_t *latest,
                                 const char *conflict_original,
                                 const char *conflict_modified,
                                 const char *conflict_latest,
                                 const char *conflict_separator,
                                 svn_boolean_t display_original_in_conflict,
                                 svn_boolean_t display_resolved_conflicts,
                                 apr_pool_t *pool)
{
  svn_diff_conflict_display_style_t style =
    svn_diff_conflict_display_modified_latest;

  if (display_resolved_conflicts)
    style = svn_diff_conflict_display_resolved_modified_latest;

  if (display_original_in_conflict)
    style = svn_diff_conflict_display_modified_original_latest;

  return svn_diff_mem_string_output_merge2(output_stream,
                                           diff,
                                           original,
                                           modified,
                                           latest,
                                           conflict_original,
                                           conflict_modified,
                                           conflict_latest,
                                           conflict_separator,
                                           style,
                                           pool);
}
