/*
 * stat.c :  file and directory stat and read functions
 *
 * ====================================================================
 *    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 APR_WANT_STRFUNC
#include <apr_want.h>

#include <serf.h>

#include "svn_private_config.h"
#include "svn_pools.h"
#include "svn_xml.h"
#include "../libsvn_ra/ra_loader.h"
#include "svn_config.h"
#include "svn_dirent_uri.h"
#include "svn_hash.h"
#include "svn_path.h"
#include "svn_props.h"
#include "svn_time.h"
#include "svn_version.h"

#include "private/svn_dav_protocol.h"
#include "private/svn_dep_compat.h"
#include "private/svn_fspath.h"

#include "ra_serf.h"



/* Implements svn_ra__vtable_t.check_path(). */
svn_error_t *
svn_ra_serf__check_path(svn_ra_session_t *ra_session,
                        const char *relpath,
                        svn_revnum_t revision,
                        svn_node_kind_t *kind,
                        apr_pool_t *scratch_pool)
{
  svn_ra_serf__session_t *session = ra_session->priv;
  apr_hash_t *props;
  svn_error_t *err;
  const char *url;

  url = session->session_url.path;

  /* If we have a relative path, append it. */
  if (relpath)
    url = svn_path_url_add_component2(url, relpath, scratch_pool);

  /* If we were given a specific revision, get a URL that refers to that
     specific revision (rather than floating with HEAD).  */
  if (SVN_IS_VALID_REVNUM(revision))
    {
      SVN_ERR(svn_ra_serf__get_stable_url(&url, NULL /* latest_revnum */,
                                          session,
                                          url, revision,
                                          scratch_pool, scratch_pool));
    }

  /* URL is stable, so we use SVN_INVALID_REVNUM since it is now irrelevant.
     Or we started with SVN_INVALID_REVNUM and URL may be floating.  */
  err = svn_ra_serf__fetch_node_props(&props, session,
                                      url, SVN_INVALID_REVNUM,
                                      check_path_props,
                                      scratch_pool, scratch_pool);

  if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND)
    {
      svn_error_clear(err);
      *kind = svn_node_none;
    }
  else
    {
      apr_hash_t *dav_props;
      const char *res_type;

      /* Any other error, raise to caller. */
      SVN_ERR(err);

      dav_props = apr_hash_get(props, "DAV:", 4);
      res_type = svn_prop_get_value(dav_props, "resourcetype");
      if (!res_type)
        {
          /* How did this happen? */
          return svn_error_create(SVN_ERR_RA_DAV_PROPS_NOT_FOUND, NULL,
                                 _("The PROPFIND response did not include the "
                                   "requested resourcetype value"));
        }

      if (strcmp(res_type, "collection") == 0)
        *kind = svn_node_dir;
      else
        *kind = svn_node_file;
    }

  return SVN_NO_ERROR;
}


/* Baton for fill_dirent_propfunc() */
struct fill_dirent_baton_t
{
  /* Update the fields in this entry.  */
  svn_dirent_t *entry;

  svn_tristate_t *supports_deadprop_count;

  /* If allocations are necessary, then use this pool.  */
  apr_pool_t *result_pool;
};

/* Implements svn_ra_serf__prop_func_t */
static svn_error_t *
fill_dirent_propfunc(void *baton,
                     const char *path,
                     const char *ns,
                     const char *name,
                     const svn_string_t *val,
                     apr_pool_t *scratch_pool)
{
  struct fill_dirent_baton_t *fdb = baton;

  if (strcmp(ns, "DAV:") == 0)
    {
      if (strcmp(name, SVN_DAV__VERSION_NAME) == 0)
        {
          apr_int64_t rev;
          SVN_ERR(svn_cstring_atoi64(&rev, val->data));

          fdb->entry->created_rev = (svn_revnum_t)rev;
        }
      else if (strcmp(name, "creator-displayname") == 0)
        {
          fdb->entry->last_author = apr_pstrdup(fdb->result_pool, val->data);
        }
      else if (strcmp(name, SVN_DAV__CREATIONDATE) == 0)
        {
          SVN_ERR(svn_time_from_cstring(&fdb->entry->time,
                                        val->data,
                                        fdb->result_pool));
        }
      else if (strcmp(name, "getcontentlength") == 0)
        {
          /* 'getcontentlength' property is empty for directories. */
          if (val->len)
            {
              SVN_ERR(svn_cstring_atoi64(&fdb->entry->size, val->data));
            }
        }
      else if (strcmp(name, "resourcetype") == 0)
        {
          if (strcmp(val->data, "collection") == 0)
            {
              fdb->entry->kind = svn_node_dir;
            }
          else
            {
              fdb->entry->kind = svn_node_file;
            }
        }
    }
  else if (strcmp(ns, SVN_DAV_PROP_NS_CUSTOM) == 0)
    {
      fdb->entry->has_props = TRUE;
    }
  else if (strcmp(ns, SVN_DAV_PROP_NS_SVN) == 0)
    {
      fdb->entry->has_props = TRUE;
    }
  else if (strcmp(ns, SVN_DAV_PROP_NS_DAV) == 0)
    {
      if(strcmp(name, "deadprop-count") == 0)
        {
          if (*val->data)
            {
              apr_int64_t deadprop_count;
              SVN_ERR(svn_cstring_atoi64(&deadprop_count, val->data));
              fdb->entry->has_props = deadprop_count > 0;
              if (fdb->supports_deadprop_count)
                *fdb->supports_deadprop_count = svn_tristate_true;
            }
          else if (fdb->supports_deadprop_count)
            *fdb->supports_deadprop_count = svn_tristate_false;
        }
    }

  return SVN_NO_ERROR;
}

static const svn_ra_serf__dav_props_t *
get_dirent_props(apr_uint32_t dirent_fields,
                 svn_ra_serf__session_t *session,
                 apr_pool_t *pool)
{
  svn_ra_serf__dav_props_t *prop;
  apr_array_header_t *props = apr_array_make
    (pool, 7, sizeof(svn_ra_serf__dav_props_t));

  if (session->supports_deadprop_count != svn_tristate_false
      || ! (dirent_fields & SVN_DIRENT_HAS_PROPS))
    {
      if (dirent_fields & SVN_DIRENT_KIND)
        {
          prop = apr_array_push(props);
          prop->xmlns = "DAV:";
          prop->name = "resourcetype";
        }

      if (dirent_fields & SVN_DIRENT_SIZE)
        {
          prop = apr_array_push(props);
          prop->xmlns = "DAV:";
          prop->name = "getcontentlength";
        }

      if (dirent_fields & SVN_DIRENT_HAS_PROPS)
        {
          prop = apr_array_push(props);
          prop->xmlns = SVN_DAV_PROP_NS_DAV;
          prop->name = "deadprop-count";
        }

      if (dirent_fields & SVN_DIRENT_CREATED_REV)
        {
          svn_ra_serf__dav_props_t *p = apr_array_push(props);
          p->xmlns = "DAV:";
          p->name = SVN_DAV__VERSION_NAME;
        }

      if (dirent_fields & SVN_DIRENT_TIME)
        {
          prop = apr_array_push(props);
          prop->xmlns = "DAV:";
          prop->name = SVN_DAV__CREATIONDATE;
        }

      if (dirent_fields & SVN_DIRENT_LAST_AUTHOR)
        {
          prop = apr_array_push(props);
          prop->xmlns = "DAV:";
          prop->name = "creator-displayname";
        }
    }
  else
    {
      /* We found an old subversion server that can't handle
         the deadprop-count property in the way we expect.

         The neon behavior is to retrieve all properties in this case */
      prop = apr_array_push(props);
      prop->xmlns = "DAV:";
      prop->name = "allprop";
    }

  prop = apr_array_push(props);
  prop->xmlns = NULL;
  prop->name = NULL;

  return (svn_ra_serf__dav_props_t *) props->elts;
}

/* Implements svn_ra__vtable_t.stat(). */
svn_error_t *
svn_ra_serf__stat(svn_ra_session_t *ra_session,
                  const char *relpath,
                  svn_revnum_t revision,
                  svn_dirent_t **dirent,
                  apr_pool_t *pool)
{
  svn_ra_serf__session_t *session = ra_session->priv;
  svn_error_t *err;
  struct fill_dirent_baton_t fdb;
  svn_tristate_t deadprop_count = svn_tristate_unknown;
  svn_ra_serf__handler_t *handler;
  const char *url;

  url = session->session_url.path;

  /* If we have a relative path, append it. */
  if (relpath)
    url = svn_path_url_add_component2(url, relpath, pool);

  /* If we were given a specific revision, get a URL that refers to that
     specific revision (rather than floating with HEAD).  */
  if (SVN_IS_VALID_REVNUM(revision))
    {
      SVN_ERR(svn_ra_serf__get_stable_url(&url, NULL /* latest_revnum */,
                                          session,
                                          url, revision,
                                          pool, pool));
    }

  fdb.entry = svn_dirent_create(pool);
  fdb.supports_deadprop_count = &deadprop_count;
  fdb.result_pool = pool;

  SVN_ERR(svn_ra_serf__create_propfind_handler(&handler, session, url,
                                               SVN_INVALID_REVNUM, "0",
                                               get_dirent_props(SVN_DIRENT_ALL,
                                                                session,
                                                                pool),
                                               fill_dirent_propfunc, &fdb, pool));

  err = svn_ra_serf__context_run_one(handler, pool);

  if (err)
    {
      if (err->apr_err == SVN_ERR_FS_NOT_FOUND)
        {
          svn_error_clear(err);
          *dirent = NULL;
          return SVN_NO_ERROR;
        }
      else
        return svn_error_trace(err);
    }

  if (deadprop_count == svn_tristate_false
      && session->supports_deadprop_count == svn_tristate_unknown
      && !fdb.entry->has_props)
    {
      /* We have to requery as the server didn't give us the right
         information */
      session->supports_deadprop_count = svn_tristate_false;

      /* Run the same handler again */
      SVN_ERR(svn_ra_serf__context_run_one(handler, pool));
    }

  if (deadprop_count != svn_tristate_unknown)
    session->supports_deadprop_count = deadprop_count;

  *dirent = fdb.entry;

  return SVN_NO_ERROR;
}

/* Baton for get_dir_dirents_cb and get_dir_props_cb */
struct get_dir_baton_t
{
  apr_pool_t *result_pool;
  apr_hash_t *dirents;
  apr_hash_t *ret_props;
  svn_boolean_t is_directory;
  svn_tristate_t supports_deadprop_count;
  const char *path;
};

/* Implements svn_ra_serf__prop_func_t */
static svn_error_t *
get_dir_dirents_cb(void *baton,
                   const char *path,
                   const char *ns,
                   const char *name,
                   const svn_string_t *value,
                   apr_pool_t *scratch_pool)
{
  struct get_dir_baton_t *db = baton;
  const char *relpath;

  relpath = svn_fspath__skip_ancestor(db->path, path);

  if (relpath && relpath[0] != '\0')
    {
      struct fill_dirent_baton_t fdb;

      relpath = svn_path_uri_decode(relpath, scratch_pool);
      fdb.entry = svn_hash_gets(db->dirents, relpath);

      if (!fdb.entry)
        {
          fdb.entry = svn_dirent_create(db->result_pool);
          svn_hash_sets(db->dirents,
                        apr_pstrdup(db->result_pool, relpath),
                        fdb.entry);
        }

      fdb.result_pool = db->result_pool;
      fdb.supports_deadprop_count = &db->supports_deadprop_count;
      SVN_ERR(fill_dirent_propfunc(&fdb, path, ns, name, value, scratch_pool));
    }
  else if (relpath && !db->is_directory)
    {
      if (strcmp(ns, "DAV:") == 0 && strcmp(name, "resourcetype") == 0)
        {
          if (strcmp(value->data, "collection") != 0)
            {
              /* Tell a lie to exit early */
              return svn_error_create(SVN_ERR_FS_NOT_DIRECTORY, NULL,
                                      _("Can't get properties of non-directory"));
            }
          else
            db->is_directory = TRUE;
        }
    }

  return SVN_NO_ERROR;
}

/* Implements svn_ra_serf__prop_func */
static svn_error_t *
get_dir_props_cb(void *baton,
                 const char *path,
                 const char *ns,
                 const char *name,
                 const svn_string_t *value,
                 apr_pool_t *scratch_pool)
{
  struct get_dir_baton_t *db = baton;
  const char *propname;

  propname = svn_ra_serf__svnname_from_wirename(ns, name, db->result_pool);
  if (propname)
    {
      svn_hash_sets(db->ret_props, propname,
                    svn_string_dup(value, db->result_pool));
      return SVN_NO_ERROR;
    }

  if (!db->is_directory)
    {
      if (strcmp(ns, "DAV:") == 0 && strcmp(name, "resourcetype") == 0)
        {
          if (strcmp(value->data, "collection") != 0)
            {
              /* Tell a lie to exit early */
              return svn_error_create(SVN_ERR_FS_NOT_DIRECTORY, NULL,
                                      _("Can't get properties of non-directory"));
            }
          else
            db->is_directory = TRUE;
        }
    }

  return SVN_NO_ERROR;
}

/* Implements svn_ra__vtable_t.get_dir(). */
svn_error_t *
svn_ra_serf__get_dir(svn_ra_session_t *ra_session,
                     apr_hash_t **dirents,
                     svn_revnum_t *fetched_rev,
                     apr_hash_t **ret_props,
                     const char *rel_path,
                     svn_revnum_t revision,
                     apr_uint32_t dirent_fields,
                     apr_pool_t *result_pool)
{
  svn_ra_serf__session_t *session = ra_session->priv;
  apr_pool_t *scratch_pool = svn_pool_create(result_pool);
  svn_ra_serf__handler_t *dirent_handler = NULL;
  svn_ra_serf__handler_t *props_handler = NULL;
  const char *path;
  struct get_dir_baton_t gdb;
  svn_error_t *err = SVN_NO_ERROR;

  gdb.result_pool = result_pool;
  gdb.is_directory = FALSE;
  gdb.supports_deadprop_count = svn_tristate_unknown;

  path = session->session_url.path;

  /* If we have a relative path, URI encode and append it. */
  if (rel_path)
    {
      path = svn_path_url_add_component2(path, rel_path, scratch_pool);
    }

  /* If the user specified a peg revision other than HEAD, we have to fetch
     the baseline collection url for that revision. If not, we can use the
     public url. */
  if (SVN_IS_VALID_REVNUM(revision) || fetched_rev)
    {
      SVN_ERR(svn_ra_serf__get_stable_url(&path, fetched_rev,
                                          session,
                                          path, revision,
                                          scratch_pool, scratch_pool));
      revision = SVN_INVALID_REVNUM;
    }
  /* REVISION is always SVN_INVALID_REVNUM  */
  SVN_ERR_ASSERT(!SVN_IS_VALID_REVNUM(revision));

  gdb.path = path;

  /* If we're asked for children, fetch them now. */
  if (dirents)
    {
      /* Always request node kind to check that path is really a
       * directory. */
      if (!ret_props)
        dirent_fields |= SVN_DIRENT_KIND;

      gdb.dirents = apr_hash_make(result_pool);

      SVN_ERR(svn_ra_serf__create_propfind_handler(
                                          &dirent_handler, session,
                                          path, SVN_INVALID_REVNUM, "1",
                                          get_dirent_props(dirent_fields,
                                                           session,
                                                           scratch_pool),
                                          get_dir_dirents_cb, &gdb,
                                          scratch_pool));

      svn_ra_serf__request_create(dirent_handler);
    }
  else
    gdb.dirents = NULL;

  if (ret_props)
    {
      gdb.ret_props = apr_hash_make(result_pool);
      SVN_ERR(svn_ra_serf__create_propfind_handler(
                                          &props_handler, session,
                                          path, SVN_INVALID_REVNUM, "0",
                                          all_props,
                                          get_dir_props_cb, &gdb,
                                          scratch_pool));

      svn_ra_serf__request_create(props_handler);
    }
  else
    gdb.ret_props = NULL;

  if (dirent_handler)
    {
      err = svn_error_trace(
              svn_ra_serf__context_run_wait(&dirent_handler->done,
                                            session,
                                            scratch_pool));

      if (err)
        {
          svn_pool_clear(scratch_pool); /* Unregisters outstanding requests */
          return err;
        }

      if (gdb.supports_deadprop_count == svn_tristate_false
          && session->supports_deadprop_count == svn_tristate_unknown
          && dirent_fields & SVN_DIRENT_HAS_PROPS)
        {
          /* We have to requery as the server didn't give us the right
             information */
          session->supports_deadprop_count = svn_tristate_false;

          apr_hash_clear(gdb.dirents);

          SVN_ERR(svn_ra_serf__create_propfind_handler(
                                              &dirent_handler, session,
                                              path, SVN_INVALID_REVNUM, "1",
                                              get_dirent_props(dirent_fields,
                                                               session,
                                                               scratch_pool),
                                              get_dir_dirents_cb, &gdb,
                                              scratch_pool));

          svn_ra_serf__request_create(dirent_handler);
        }
    }

  if (props_handler)
    {
      err = svn_error_trace(
              svn_ra_serf__context_run_wait(&props_handler->done,
                                            session,
                                            scratch_pool));
    }

  /* And dirent again for the case when we had to send the request again */
  if (! err && dirent_handler)
    {
      err = svn_error_trace(
              svn_ra_serf__context_run_wait(&dirent_handler->done,
                                            session,
                                            scratch_pool));
    }

  if (!err && gdb.supports_deadprop_count != svn_tristate_unknown)
    session->supports_deadprop_count = gdb.supports_deadprop_count;

  svn_pool_destroy(scratch_pool); /* Unregisters outstanding requests */

  SVN_ERR(err);

  if (!gdb.is_directory)
    return svn_error_create(SVN_ERR_FS_NOT_DIRECTORY, NULL,
                            _("Can't get entries of non-directory"));

  if (ret_props)
    *ret_props = gdb.ret_props;

  if (dirents)
    *dirents = gdb.dirents;

  return SVN_NO_ERROR;
}
