/*
 * replay.c :  routines for replaying revisions
 *
 * ====================================================================
 * Copyright (c) 2005 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */

#include "svn_base64.h"
#include "svn_pools.h"
#include "svn_xml.h"

#include "../libsvn_ra/ra_loader.h"

#include "ra_dav.h"

typedef struct {
  /* The underlying editor and baton we're replaying into. */
  const svn_delta_editor_t *editor;
  void *edit_baton;

  /* Any error that occurs during the replay is stored here, so it can be
   * returned after we bail out of the XML parsing. */
  svn_error_t *err;

  /* Parent pool for the whole reply. */
  apr_pool_t *pool;

  /* Stack of in progress directories, holds dir_item_t objects. */
  apr_array_header_t *dirs;

  /* Cached file baton so we can pass it between the add/open file and
   * apply textdelta portions of the editor drive. */
  void *file_baton;

  /* Variables required to decode and apply our svndiff data off the wire. */
  svn_txdelta_window_handler_t whandler;
  void *whandler_baton;
  svn_stream_t *svndiff_decoder;
  svn_stream_t *base64_decoder;

  /* A scratch pool used to allocate property data. */
  apr_pool_t *prop_pool;

  /* The name of a property that's being modified. */
  const char *prop_name;

  /* A stringbuf that holds the contents of a property being changed, if this
   * is NULL it means that the property is being deleted. */
  svn_stringbuf_t *prop_accum;
} replay_baton_t;

#define TOP_DIR(rb) (((dir_item_t *)(rb)->dirs->elts)[(rb)->dirs->nelts - 1])

/* Info about a given directory we've seen. */
typedef struct {
  void *baton;
  const char *path;
  apr_pool_t *pool;
  apr_pool_t *file_pool;
} dir_item_t;

static void
push_dir(replay_baton_t *rb, void *baton, const char *path, apr_pool_t *pool)
{
  dir_item_t *di = apr_array_push(rb->dirs);

  di->baton = baton;
  di->path = apr_pstrdup(pool, path);
  di->pool = pool;
  di->file_pool = svn_pool_create(pool);
}

static const svn_ra_dav__xml_elm_t editor_report_elements[] =
{
  { SVN_XML_NAMESPACE, "editor-report",    ELEM_editor_report, 0 },
  { SVN_XML_NAMESPACE, "target-revision",  ELEM_target_revision, 0 },
  { SVN_XML_NAMESPACE, "open-root",        ELEM_open_root, 0 },
  { SVN_XML_NAMESPACE, "delete-entry",     ELEM_delete_entry, 0 },
  { SVN_XML_NAMESPACE, "open-directory",   ELEM_open_directory, 0 },
  { SVN_XML_NAMESPACE, "add-directory",    ELEM_add_directory, 0 },
  { SVN_XML_NAMESPACE, "open-file",        ELEM_open_file, 0 },
  { SVN_XML_NAMESPACE, "add-file",         ELEM_add_file, 0 },
  { SVN_XML_NAMESPACE, "close-file",       ELEM_close_file, 0 },
  { SVN_XML_NAMESPACE, "close-directory",  ELEM_close_directory, 0 },
  { SVN_XML_NAMESPACE, "apply-textdelta",  ELEM_apply_textdelta, 0 },
  { SVN_XML_NAMESPACE, "change-file-prop", ELEM_change_file_prop, 0 },
  { SVN_XML_NAMESPACE, "change-dir-prop",  ELEM_change_dir_prop, 0 },
  { NULL }
};

static int
start_element(void *baton, int parent_state, const char *nspace,
              const char *elt_name, const char **atts)
{
  replay_baton_t *rb = baton;

  const svn_ra_dav__xml_elm_t *elm
    = svn_ra_dav__lookup_xml_elem(editor_report_elements, nspace, elt_name);

  if (! elm)
    return NE_XML_DECLINE;

  if (parent_state == ELEM_root)
    {
      /* If we're at the root of the tree, the element has to be the editor
       * report itself. */
      if (elm->id != ELEM_editor_report)
        return SVN_RA_DAV__XML_INVALID;
    }
  else if (parent_state != ELEM_editor_report)
    {
      /* If we're not at the root, our parent has to be the editor report,
       * since we don't actually nest any elements. */
      return SVN_RA_DAV__XML_INVALID;
    }

  switch (elm->id)
    {
    case ELEM_target_revision:
      {
        const char *crev = svn_xml_get_attr_value("rev", atts);
        if (! crev)
          rb->err = svn_error_create
                     (SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
                      _("Missing revision attr in target-revision element"));
        else
          rb->err = rb->editor->set_target_revision(rb->edit_baton,
                                                    SVN_STR_TO_REV(crev),
                                                    rb->pool);
      }
      break;

    case ELEM_open_root:
      {
        const char *crev = svn_xml_get_attr_value("rev", atts);

        if (! crev)
          rb->err = svn_error_create
                     (SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
                      _("Missing revision attr in open-root element"));
        else
          {
            apr_pool_t *subpool = svn_pool_create(rb->pool);
            void *dir_baton;
            rb->err = rb->editor->open_root(rb->edit_baton,
                                            SVN_STR_TO_REV(crev), subpool,
                                            &dir_baton);
            push_dir(rb, dir_baton, "", subpool);
          }
      }
      break;

    case ELEM_delete_entry:
      {
        const char *path = svn_xml_get_attr_value("name", atts);
        const char *crev = svn_xml_get_attr_value("rev", atts);

        if (! path)
          rb->err = svn_error_create
                      (SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
                       _("Missing name attr in delete-entry element"));
        else if (! crev)
          rb->err = svn_error_create
                      (SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
                       _("Missing rev attr in delete-entry element"));
        else
          {
            dir_item_t *di = &TOP_DIR(rb);

            rb->err = rb->editor->delete_entry(path, SVN_STR_TO_REV(crev),
                                               di->baton, di->pool);
          }
      }
      break;

    case ELEM_open_directory:
    case ELEM_add_directory:
      {
        const char *crev = svn_xml_get_attr_value("rev", atts);
        const char *name = svn_xml_get_attr_value("name", atts);

        if (! name)
          rb->err = svn_error_create
                     (SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
                      _("Missing name attr in open-directory element"));
        else
          {
            dir_item_t *parent = &TOP_DIR(rb);
            apr_pool_t *subpool = svn_pool_create(parent->pool);
            svn_revnum_t rev;
            void *dir_baton;

            if (crev)
              rev = SVN_STR_TO_REV(crev);
            else
              rev = SVN_INVALID_REVNUM;

            if (elm->id == ELEM_open_directory)
              rb->err = rb->editor->open_directory(name, parent->baton,
                                                   rev, subpool, &dir_baton);
            else if (elm->id == ELEM_add_directory)
              {
                const char *cpath = svn_xml_get_attr_value("copyfrom-path",
                                                           atts);

                crev = svn_xml_get_attr_value("copyfrom-rev", atts);

                if (crev)
                  rev = SVN_STR_TO_REV(crev);
                else
                  rev = SVN_INVALID_REVNUM;

                rb->err = rb->editor->add_directory(name, parent->baton,
                                                    cpath, rev, subpool,
                                                    &dir_baton);
              }
            else
              abort();

            push_dir(rb, dir_baton, name, subpool);
          }
      }
      break;

    case ELEM_open_file:
    case ELEM_add_file:
      {
        const char *path = svn_xml_get_attr_value("name", atts);
        svn_revnum_t rev;

        dir_item_t *parent = &TOP_DIR(rb);

        if (! path)
          {
            rb->err = svn_error_createf
                        (SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
                         _("Missing name attr in %s element"),
                         elm->id == ELEM_open_file ? "open-file" : "add-file");
            break;
          }

        svn_pool_clear(parent->file_pool);

        if (elm->id == ELEM_add_file)
          {
            const char *cpath = svn_xml_get_attr_value("copyfrom-path", atts);
            const char *crev = svn_xml_get_attr_value("copyfrom-rev", atts);

            if (crev)
              rev = SVN_STR_TO_REV(crev);
            else
              rev = SVN_INVALID_REVNUM;

            rb->err = rb->editor->add_file(path, parent->baton, cpath, rev,
                                           parent->file_pool, &rb->file_baton);
          }
        else
          {
            const char *crev = svn_xml_get_attr_value("rev", atts);

            if (crev)
              rev = SVN_STR_TO_REV(crev);
            else
              rev = SVN_INVALID_REVNUM;

            rb->err = rb->editor->open_file(path, parent->baton, rev,
                                            parent->file_pool,
                                            &rb->file_baton);
          }
      }
      break;

    case ELEM_apply_textdelta:
      if (! rb->file_baton)
        rb->err = svn_error_create
                    (SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
                     _("Got apply-textdelta element without preceding "
                       "add-file or open-file"));
      else
        {
          const char *checksum = svn_xml_get_attr_value("checksum", atts);

          rb->err = rb->editor->apply_textdelta(rb->file_baton,
                                                checksum,
                                                TOP_DIR(rb).file_pool,
                                                &rb->whandler,
                                                &rb->whandler_baton);
          if (! rb->err)
            {
              rb->svndiff_decoder = svn_txdelta_parse_svndiff
                                      (rb->whandler, rb->whandler_baton,
                                       TRUE, TOP_DIR(rb).file_pool);
              rb->base64_decoder = svn_base64_decode(rb->svndiff_decoder,
                                                     TOP_DIR(rb).file_pool);
            }
        }
      break;

    case ELEM_close_file:
      if (! rb->file_baton)
        rb->err = svn_error_create
                    (SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
                     _("Got close-file element without preceding "
                       "add-file or open-file"));
      else
        {
          rb->err = rb->editor->close_file(rb->file_baton,
                                           NULL, /* XXX text checksum */
                                           TOP_DIR(rb).file_pool);
          rb->file_baton = NULL;
        }
      break;

    case ELEM_close_directory:
      if (rb->dirs->nelts == 0)
        rb->err = svn_error_create
                    (SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
                     _("Got close-directory element without ever opening "
                       "a directory"));
      else
        {
          dir_item_t *di = &TOP_DIR(rb);

          rb->err = rb->editor->close_directory(di->baton, di->pool);

          svn_pool_destroy(di->pool);

          apr_array_pop(rb->dirs);
        }
      break;

    case ELEM_change_file_prop:
    case ELEM_change_dir_prop:
      {
        const char *name = svn_xml_get_attr_value("name", atts);

        if (! name)
          rb->err = svn_error_createf
                      (SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
                       _("Missing name attr in %s element"),
                       elm->id == ELEM_change_file_prop ? "change-file-prop"
                                                        : "change-dir-prop");
        else
          {
            svn_pool_clear(rb->prop_pool);

            if (svn_xml_get_attr_value("del", atts))
              rb->prop_accum = NULL;
            else
              rb->prop_accum = svn_stringbuf_create("", rb->prop_pool);

            rb->prop_name = apr_pstrdup(rb->prop_pool, name);
          }
      }
      break;
    }

  if (rb->err)
    return NE_XML_ABORT;

  return elm->id;
}

static int
end_element(void *baton, int state, const char *nspace, const char *elt_name)
{
  replay_baton_t *rb = baton;

  const svn_ra_dav__xml_elm_t *elm
    = svn_ra_dav__lookup_xml_elem(editor_report_elements, nspace, elt_name);

  if (! elm)
    return NE_XML_DECLINE;

  switch (elm->id)
    {
    case ELEM_editor_report:
      if (rb->dirs->nelts)
        svn_pool_destroy(APR_ARRAY_IDX(rb->dirs, 0, dir_item_t).pool);

      rb->err = SVN_NO_ERROR;
      break;

    case ELEM_apply_textdelta:
      rb->err = svn_stream_close(rb->base64_decoder);

      rb->whandler = NULL;
      rb->whandler_baton = NULL;
      rb->svndiff_decoder = NULL;
      rb->base64_decoder = NULL;
      break;

    case ELEM_change_file_prop:
    case ELEM_change_dir_prop:
      {
        const svn_string_t *decoded_value;
        svn_string_t prop;

        if (rb->prop_accum)
          {
            prop.data = rb->prop_accum->data;
            prop.len = rb->prop_accum->len;

            decoded_value = svn_base64_decode_string(&prop, rb->prop_pool);
          }
        else
          decoded_value = NULL; /* It's a delete */

        if (elm->id == ELEM_change_dir_prop)
          rb->err = rb->editor->change_dir_prop(TOP_DIR(rb).baton,
                                                rb->prop_name,
                                                decoded_value,
                                                TOP_DIR(rb).pool);
        else
          rb->err = rb->editor->change_file_prop(rb->file_baton,
                                                 rb->prop_name,
                                                 decoded_value,
                                                 TOP_DIR(rb).file_pool);
      }
      break;

    default:
      break;
    }

  if (rb->err)
    return NE_XML_ABORT;

  return SVN_RA_DAV__XML_VALID;
}

static int
cdata_handler(void *baton, int state, const char *cdata, size_t len)
{
  replay_baton_t *rb = baton;
  apr_size_t nlen = len;

  switch (state)
    {
    case ELEM_apply_textdelta:
      rb->err = svn_stream_write(rb->base64_decoder, cdata, &nlen);
      if (! rb->err && nlen != len)
        rb->err = svn_error_createf
                    (SVN_ERR_STREAM_UNEXPECTED_EOF, NULL,
                     _("Error writing stream: unexpected EOF"));
      break;

    case ELEM_change_dir_prop:
    case ELEM_change_file_prop:
      if (! rb->prop_accum)
        rb->err = svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
                                    _("Got cdata content for a prop delete"));
      else
        svn_stringbuf_appendbytes(rb->prop_accum, cdata, len);
      break;
    }

  if (rb->err)
    return NE_XML_ABORT;

  return 0; /* no error */
}

svn_error_t *
svn_ra_dav__replay(svn_ra_session_t *session,
                   svn_revnum_t revision,
                   svn_revnum_t low_water_mark,
                   svn_boolean_t send_deltas,
                   const svn_delta_editor_t *editor,
                   void *edit_baton,
                   apr_pool_t *pool)
{
  svn_ra_dav__session_t *ras = session->priv;
  replay_baton_t rb;

  const char *body
    = apr_psprintf(pool,
                   "<S:replay-report xmlns:S=\"svn:\">\n"
                   "  <S:revision>%ld</S:revision>\n"
                   "  <S:low-water-mark>%ld</S:low-water-mark>\n"
                   "  <S:send-deltas>%d</S:send-deltas>\n"
                   "</S:replay-report>",
                   revision, low_water_mark, send_deltas);

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

  rb.editor = editor;
  rb.edit_baton = edit_baton;
  rb.err = SVN_NO_ERROR;
  rb.pool = pool;
  rb.dirs = apr_array_make(pool, 5, sizeof(dir_item_t));
  rb.prop_pool = svn_pool_create(pool);
  rb.prop_accum = svn_stringbuf_create("", rb.prop_pool);

  SVN_ERR(svn_ra_dav__parsed_request(ras->sess, "REPORT", ras->url->data, body,
                                     NULL, NULL,
                                     start_element,
                                     cdata_handler,
                                     end_element,
                                     &rb,
                                     NULL, /* extra headers */
                                     NULL, /* status code */
                                     FALSE, /* spool response */
                                     pool));

  return rb.err;
}
