/**
 * @copyright
 * ====================================================================
 *    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.
 * ====================================================================
 * @endcopyright
 *
 * @file OperationContext.cpp
 * @brief Implementation of the class OperationContext
 */

#include <apr_file_io.h>

#include "GlobalConfig.h"
#include "OperationContext.h"
#include "JNIUtil.h"
#include "JNICriticalSection.h"

#include "Prompter.h"
#include "CreateJ.h"
#include "EnumMapper.h"
#include "CommitMessage.h"

#include "svn_client.h"
#include "private/svn_wc_private.h"
#include "private/svn_dep_compat.h"
#include "svn_private_config.h"

OperationContext::OperationContext(SVN::Pool &pool)
  : m_config(NULL),
    m_prompter(),
    m_cancelOperation(0),
    m_pool(&pool),
    m_jctx(NULL),
    m_jcfgcb(NULL),
    m_jtunnelcb(NULL)
{}

void
OperationContext::attachJavaObject(
    jobject contextHolder, const char *contextClassType,
    const char *contextFieldName, jfieldID * ctxFieldID)
{
  JNIEnv *env = JNIUtil::getEnv();

  /* Grab a global reference to the Java object embedded in the parent
     Java object. */
  if ((*ctxFieldID) == 0)
    {
      jclass clazz = env->GetObjectClass(contextHolder);
      if (JNIUtil::isJavaExceptionThrown())
        return;

      (*ctxFieldID) = env->GetFieldID(clazz, contextFieldName, contextClassType);
      if (JNIUtil::isJavaExceptionThrown() || (*ctxFieldID) == 0)
        return;

      env->DeleteLocalRef(clazz);
    }

  jobject jctx = env->GetObjectField(contextHolder, (*ctxFieldID));
  if (JNIUtil::isJavaExceptionThrown())
    return;

  m_jctx = env->NewGlobalRef(jctx);
  if (JNIUtil::isJavaExceptionThrown())
    return;

  env->DeleteLocalRef(jctx);
}

OperationContext::~OperationContext()
{
  JNIEnv *env = JNIUtil::getEnv();
  env->DeleteGlobalRef(m_jctx);
  if (m_jcfgcb)
    env->DeleteGlobalRef(m_jcfgcb);
  if (m_jtunnelcb)
    env->DeleteGlobalRef(m_jtunnelcb);
}

apr_hash_t *
OperationContext::getConfigData()
{
  if(m_pool->getPool() == NULL)
    {
      JNIUtil::throwNullPointerException("pool is null");
    }

  if (m_config == NULL)
    {
      const char *configDir = m_configDir.c_str();
      if (m_configDir.empty())
        configDir = NULL;
      SVN_JNI_ERR(
          svn_config_get_config(&m_config, configDir, m_pool->getPool()), NULL);
      notifyConfigLoad();
    }

  return m_config;
}

svn_auth_baton_t *
OperationContext::getAuthBaton(SVN::Pool &in_pool)
{
  svn_auth_baton_t *ab;
  apr_pool_t *pool = in_pool.getPool();

  apr_hash_t * configData = getConfigData();

  if (configData == NULL)
    {
      return NULL;
    }

  svn_config_t *config = static_cast<svn_config_t *>(apr_hash_get(configData,
      SVN_CONFIG_CATEGORY_CONFIG, APR_HASH_KEY_STRING));

  const bool use_native_store = GlobalConfig::useNativeCredentialsStore();

  /* The whole list of registered providers */
  apr_array_header_t *providers;
  svn_auth_provider_object_t *provider;

  if (use_native_store)
    {
      /* Populate the registered providers with the platform-specific providers */
      SVN_JNI_ERR(
          svn_auth_get_platform_specific_client_providers(
              &providers, config, pool),
          NULL);

      /* Use the prompter (if available) to prompt for password and cert
       * caching. */
      svn_auth_plaintext_prompt_func_t plaintext_prompt_func;
      void *plaintext_prompt_baton;
      svn_auth_plaintext_passphrase_prompt_func_t plaintext_passphrase_prompt_func;
      void *plaintext_passphrase_prompt_baton;

      if (m_prompter.get())
        {
          plaintext_prompt_func = Prompter::plaintext_prompt;
          plaintext_prompt_baton = m_prompter.get();
          plaintext_passphrase_prompt_func = Prompter::plaintext_passphrase_prompt;
          plaintext_passphrase_prompt_baton = m_prompter.get();
        }
      else
        {
          plaintext_prompt_func = NULL;
          plaintext_prompt_baton = NULL;
          plaintext_passphrase_prompt_func = NULL;
          plaintext_passphrase_prompt_baton = NULL;
        }

      /* The main disk-caching auth providers, for both
       * 'username/password' creds and 'username' creds.  */

      svn_auth_get_simple_provider2(&provider, plaintext_prompt_func,
                                    plaintext_prompt_baton, pool);
      APR_ARRAY_PUSH(providers, svn_auth_provider_object_t *) = provider;

      svn_auth_get_username_provider(&provider, pool);
      APR_ARRAY_PUSH(providers, svn_auth_provider_object_t *) = provider;

      svn_auth_get_ssl_server_trust_file_provider(&provider, pool);
      APR_ARRAY_PUSH(providers, svn_auth_provider_object_t *) = provider;
      svn_auth_get_ssl_client_cert_file_provider(&provider, pool);
      APR_ARRAY_PUSH(providers, svn_auth_provider_object_t *) = provider;
      svn_auth_get_ssl_client_cert_pw_file_provider2(
          &provider,
          plaintext_passphrase_prompt_func, plaintext_passphrase_prompt_baton,
          pool);
      APR_ARRAY_PUSH(providers, svn_auth_provider_object_t *) = provider;
    }
  else
    {
      // Not using hte native credentials store, start with an empty
      // providers array.
      providers = apr_array_make(pool, 0, sizeof(svn_auth_provider_object_t *));
    }

  if (m_prompter.get())
    {
      /* Two basic prompt providers: username/password, and just username.*/
      provider = m_prompter->get_provider_simple(in_pool);
      APR_ARRAY_PUSH(providers, svn_auth_provider_object_t *) = provider;

      provider = m_prompter->get_provider_username(in_pool);
      APR_ARRAY_PUSH(providers, svn_auth_provider_object_t *) = provider;

      /* Three ssl prompt providers, for server-certs, client-certs,
       * and client-cert-passphrases.  */
      provider = m_prompter->get_provider_server_ssl_trust(in_pool);
      APR_ARRAY_PUSH(providers, svn_auth_provider_object_t *) = provider;

      provider = m_prompter->get_provider_client_ssl(in_pool);
      APR_ARRAY_PUSH(providers, svn_auth_provider_object_t *) = provider;

      provider = m_prompter->get_provider_client_ssl_password(in_pool);
      APR_ARRAY_PUSH(providers, svn_auth_provider_object_t *) = provider;
    }

  /* Build an authentication baton to give to libsvn_client. */
  svn_auth_open(&ab, providers, pool);

  /* Place any default --username or --password credentials into the
   * auth_baton's run-time parameter hash.  ### Same with --no-auth-cache? */
  if (!m_userName.empty())
    svn_auth_set_parameter(ab, SVN_AUTH_PARAM_DEFAULT_USERNAME,
        apr_pstrdup(in_pool.getPool(), m_userName.c_str()));
  if (!m_passWord.empty())
    svn_auth_set_parameter(ab, SVN_AUTH_PARAM_DEFAULT_PASSWORD,
        apr_pstrdup(in_pool.getPool(), m_passWord.c_str()));
  /* Store where to retrieve authentication data? */
  if (!m_configDir.empty())
    svn_auth_set_parameter(ab, SVN_AUTH_PARAM_CONFIG_DIR,
        apr_pstrdup(in_pool.getPool(), m_configDir.c_str()));
  return ab;
}

jobject OperationContext::getSelf() const
{
  return m_jctx;
}

void
OperationContext::username(const char *pi_username)
{
  m_userName = (pi_username == NULL ? "" : pi_username);
}

void
OperationContext::password(const char *pi_password)
{
  m_passWord = (pi_password == NULL ? "" : pi_password);
}

void
OperationContext::setPrompt(Prompter::UniquePtr prompter)
{
  m_prompter = JavaHL::cxx::move(prompter);
}

void
OperationContext::setConfigDirectory(const char *configDir)
{
  // A change to the config directory may necessitate creation of
  // the config templates.
  SVN::Pool requestPool;
  SVN_JNI_ERR(svn_config_ensure(configDir, requestPool.getPool()), );

  m_configDir = (configDir == NULL ? "" : configDir);

  m_config = NULL;
}

const char *
OperationContext::getConfigDirectory() const
{
  return (m_configDir.empty() ? NULL : m_configDir.c_str());
}

void OperationContext::setConfigEventHandler(jobject jcfgcb)
{
  JNIEnv *env = JNIUtil::getEnv();
  if (jcfgcb)
    {
      jcfgcb = env->NewGlobalRef(jcfgcb);
      if (JNIUtil::isJavaExceptionThrown())
        return;
    }

  if (m_jcfgcb)
    env->DeleteGlobalRef(m_jcfgcb);
  m_jcfgcb = jcfgcb;
}

jobject OperationContext::getConfigEventHandler() const
{
  return m_jcfgcb;
}

const char *
OperationContext::getUsername() const
{
  return (m_userName.empty() ? NULL : m_userName.c_str());
}

const char *
OperationContext::getPassword() const
{
  return (m_passWord.empty() ? NULL : m_passWord.c_str());
}

Prompter::UniquePtr OperationContext::clonePrompter() const
{
  if (m_prompter.get())
    return m_prompter->clone();
  return Prompter::UniquePtr();
}

void OperationContext::setTunnelCallback(jobject jtunnelcb)
{
  JNIEnv *env = JNIUtil::getEnv();
  if (jtunnelcb)
    {
      jtunnelcb = env->NewGlobalRef(jtunnelcb);
      if (JNIUtil::isJavaExceptionThrown())
        return;
    }

  if (m_jtunnelcb)
    env->DeleteGlobalRef(m_jtunnelcb);
  m_jtunnelcb = jtunnelcb;
}

jobject OperationContext::getTunnelCallback() const
{
  return m_jtunnelcb;
}

void
OperationContext::cancelOperation()
{
  svn_atomic_set(&m_cancelOperation, 1);
}

void
OperationContext::resetCancelRequest()
{
  svn_atomic_set(&m_cancelOperation, 0);
}

bool
OperationContext::isCancelledOperation()
{
  return bool(svn_atomic_read(&m_cancelOperation));
}

svn_error_t *
OperationContext::checkCancel(void *cancelBaton)
{
  OperationContext *that = static_cast<OperationContext *>(cancelBaton);
  if (that->isCancelledOperation())
    return svn_error_create(SVN_ERR_CANCELLED, NULL, _("Operation cancelled"));
  else if (JNIUtil::isJavaExceptionThrown())
    return svn_error_create(SVN_ERR_CANCELLED, JNIUtil::wrapJavaException(),
                            _("Operation cancelled"));
  else
    return SVN_NO_ERROR;
}

void
OperationContext::progress(apr_off_t progressVal, apr_off_t total, void *baton,
    apr_pool_t *pool)
{
  jobject jctx = (jobject) baton;
  if (!jctx)
    return;

  JNIEnv *env = JNIUtil::getEnv();

  // Create a local frame for our references
  env->PushLocalFrame(LOCAL_FRAME_SIZE);
  if (JNIUtil::isJavaExceptionThrown())
    return;

  static jmethodID mid = 0;
  if (mid == 0)
    {
      jclass clazz = env->GetObjectClass(jctx);
      if (JNIUtil::isJavaExceptionThrown())
        POP_AND_RETURN_NOTHING();

      mid = env->GetMethodID(clazz, "onProgress",
                             "(" JAVAHL_ARG("/ProgressEvent;") ")V");
      if (JNIUtil::isJavaExceptionThrown() || mid == 0)
        POP_AND_RETURN_NOTHING();
    }

  static jmethodID midCT = 0;
  jclass clazz = env->FindClass(JAVAHL_CLASS("/ProgressEvent"));
  if (JNIUtil::isJavaExceptionThrown())
    POP_AND_RETURN_NOTHING();

  if (midCT == 0)
    {
      midCT = env->GetMethodID(clazz, "<init>", "(JJ)V");
      if (JNIUtil::isJavaExceptionThrown() || midCT == 0)
        POP_AND_RETURN_NOTHING();
    }

  // Call the Java method.
  jobject jevent = env->NewObject(clazz, midCT, (jlong) progressVal,
      (jlong) total);
  if (JNIUtil::isJavaExceptionThrown())
    POP_AND_RETURN_NOTHING();

  env->CallVoidMethod(jctx, mid, jevent);

  POP_AND_RETURN_NOTHING();
}

const char *
OperationContext::getClientName() const
{
  return "javahl";
}

svn_error_t *
OperationContext::clientName(void *baton, const char **name, apr_pool_t *pool)
{
  OperationContext *that = (OperationContext *) baton;

  *name = that->getClientName();

  return SVN_NO_ERROR;
}

void
OperationContext::notifyConfigLoad()
{
  if (!m_jcfgcb)
    return;

  JNIEnv *env = JNIUtil::getEnv();

  static jmethodID onload_mid = 0;
  if (0 == onload_mid)
    {
      jclass cls = env->FindClass(JAVAHL_CLASS("/callback/ConfigEvent"));
      if (JNIUtil::isJavaExceptionThrown())
        return;
      onload_mid = env->GetMethodID(cls, "onLoad",
                                    "(" JAVAHL_ARG("/ISVNConfig;") ")V");
      if (JNIUtil::isJavaExceptionThrown())
        return;
    }

  jclass cfg_cls = env->FindClass(JAVAHL_CLASS("/util/ConfigImpl"));
  if (JNIUtil::isJavaExceptionThrown())
    return;

  static jmethodID ctor_mid = 0;
  if (0 == ctor_mid)
    {
      ctor_mid = env->GetMethodID(cfg_cls, "<init>", "(J)V");
      if (JNIUtil::isJavaExceptionThrown())
        return;
    }

  static jmethodID dispose_mid = 0;
  if (0 == dispose_mid)
    {
      dispose_mid = env->GetMethodID(cfg_cls, "dispose", "()V");
      if (JNIUtil::isJavaExceptionThrown())
        return;
    }

  jobject jcbimpl = env->NewObject(cfg_cls, ctor_mid,
                                   reinterpret_cast<jlong>(this));
  if (JNIUtil::isJavaExceptionThrown())
    return;
  env->CallVoidMethod(m_jcfgcb, onload_mid, jcbimpl);
  if (JNIUtil::isJavaExceptionThrown())
    return;
  env->CallVoidMethod(jcbimpl, dispose_mid);
  env->DeleteLocalRef(jcbimpl);
}

namespace {
class TunnelContext
{
public:
  explicit TunnelContext(apr_pool_t *pool)
    : request_in(NULL),
      request_out(NULL),
      response_in(NULL),
      response_out(NULL),
      jrequest(NULL),
      jresponse(NULL),
      jclosecb(NULL)
    {
      status = apr_file_pipe_create_ex(&request_in, &request_out,
                                       APR_FULL_BLOCK, pool);
      if (!status)
        status = apr_file_pipe_create_ex(&response_in, &response_out,
                                         APR_FULL_BLOCK, pool);
    }

  ~TunnelContext()
    {
      apr_file_close(request_out);
      apr_file_close(response_in);
    }

  apr_file_t *request_in;
  apr_file_t *request_out;
  apr_file_t *response_in;
  apr_file_t *response_out;
  apr_status_t status;
  jobject jrequest;
  jobject jresponse;
  jobject jclosecb;
};

jobject create_Channel(const char *class_name, JNIEnv *env, apr_file_t *fd)
{
  jclass cls = env->FindClass(class_name);
  if (JNIUtil::isJavaExceptionThrown())
    return NULL;
  jmethodID ctor = env->GetMethodID(cls, "<init>", "(J)V");
  if (JNIUtil::isJavaExceptionThrown())
    return NULL;
  jobject channel = env->NewObject(cls, ctor, reinterpret_cast<jlong>(fd));
  if (JNIUtil::isJavaExceptionThrown())
    return NULL;
  return env->NewGlobalRef(channel);
}

jobject create_RequestChannel(JNIEnv *env, apr_file_t *fd)
{
  return create_Channel(JAVAHL_CLASS("/util/RequestChannel"), env, fd);
}
jobject create_ResponseChannel(JNIEnv *env, apr_file_t *fd)
{
  return create_Channel(JAVAHL_CLASS("/util/ResponseChannel"), env, fd);
}
void close_TunnelChannel(JNIEnv* env, jobject channel)
{
  // Usually after this function, the memory will be freed behind
  // 'TunnelChannel.nativeChannel'. Ask Java side to forget it. This is the
  // only way to avoid a JVM crash when 'TunnelAgent' tries to read/write,
  // not knowing that 'TunnelChannel' is already closed in native side.
  static jmethodID mid = 0;
  if (0 == mid)
    {
      jclass cls;
      SVN_JNI_CATCH_VOID(
        cls = env->FindClass(JAVAHL_CLASS("/util/TunnelChannel")));
      SVN_JNI_CATCH_VOID(mid = env->GetMethodID(cls, "syncClose", "()V"));
    }

  SVN_JNI_CATCH_VOID(env->CallVoidMethod(channel, mid));
  env->DeleteGlobalRef(channel);
}
} // anonymous namespace

svn_boolean_t
OperationContext::checkTunnel(void *tunnel_baton, const char *tunnel_name)
{
  JNIEnv *env = JNIUtil::getEnv();

  jstring jtunnel_name = JNIUtil::makeJString(tunnel_name);
  if (JNIUtil::isJavaExceptionThrown())
    return false;

  static jmethodID mid = 0;
  if (0 == mid)
    {
      jclass cls = env->FindClass(JAVAHL_CLASS("/callback/TunnelAgent"));
      if (JNIUtil::isJavaExceptionThrown())
        return false;
      mid = env->GetMethodID(cls, "checkTunnel",
                             "(Ljava/lang/String;)Z");
        if (JNIUtil::isJavaExceptionThrown())
          return false;
    }

  jobject jtunnelcb = jobject(tunnel_baton);
  jboolean check = env->CallBooleanMethod(jtunnelcb, mid, jtunnel_name);
  if (JNIUtil::isJavaExceptionThrown())
    return false;

  return svn_boolean_t(check);
}

svn_error_t *
OperationContext::openTunnel(svn_stream_t **request, svn_stream_t **response,
                             svn_ra_close_tunnel_func_t *close_func,
                             void **close_baton,
                             void *tunnel_baton,
                             const char *tunnel_name, const char *user,
                             const char *hostname, int port,
                             svn_cancel_func_t cancel_func, void *cancel_baton,
                             apr_pool_t *pool)
{
  TunnelContext *tc = new TunnelContext(pool);
  if (tc->status)
    {
      delete tc;
      return svn_error_trace(
          svn_error_wrap_apr(tc->status, _("Could not open tunnel streams")));
    }

  *close_func = closeTunnel;
  *close_baton = tc;
  *request = svn_stream_from_aprfile2(tc->request_out, FALSE, pool);
  *response = svn_stream_from_aprfile2(tc->response_in, FALSE, pool);

  JNIEnv *env = JNIUtil::getEnv();

  tc->jrequest = create_RequestChannel(env, tc->request_in);
  SVN_JNI_CATCH(, SVN_ERR_BASE);

  tc->jresponse = create_ResponseChannel(env, tc->response_out);
  SVN_JNI_CATCH(, SVN_ERR_BASE);

  jstring jtunnel_name = JNIUtil::makeJString(tunnel_name);
  SVN_JNI_CATCH(, SVN_ERR_BASE);

  jstring juser = JNIUtil::makeJString(user);
  SVN_JNI_CATCH(, SVN_ERR_BASE);

  jstring jhostname = JNIUtil::makeJString(hostname);
  SVN_JNI_CATCH(, SVN_ERR_BASE);

  static jmethodID mid = 0;
  if (0 == mid)
    {
      jclass cls = env->FindClass(JAVAHL_CLASS("/callback/TunnelAgent"));
      SVN_JNI_CATCH(, SVN_ERR_BASE);
      SVN_JNI_CATCH(
          mid = env->GetMethodID(
              cls, "openTunnel",
              "(Ljava/nio/channels/ReadableByteChannel;"
              "Ljava/nio/channels/WritableByteChannel;"
              "Ljava/lang/String;"
              "Ljava/lang/String;"
              "Ljava/lang/String;I)"
              JAVAHL_ARG("/callback/TunnelAgent$CloseTunnelCallback;")),
          SVN_ERR_BASE);
    }

  jobject jtunnelcb = jobject(tunnel_baton);
  tc->jclosecb = env->CallObjectMethod(
    jtunnelcb, mid, tc->jrequest, tc->jresponse,
    jtunnel_name, juser, jhostname, jint(port));
  svn_error_t* openTunnelError = JNIUtil::checkJavaException(SVN_ERR_BASE);
  if (SVN_NO_ERROR != openTunnelError)
    {
      // OperationContext::closeTunnel() will never be called, clean up here.
      // This also prevents a JVM native crash, see comment in
      // close_TunnelChannel().
      *close_baton = 0;
      tc->jclosecb = 0;
      OperationContext::closeTunnel(tc, 0);
      SVN_ERR(openTunnelError);
    }

  if (tc->jclosecb)
    {
      tc->jclosecb = env->NewGlobalRef(tc->jclosecb);
      SVN_JNI_CATCH(, SVN_ERR_BASE);
    }

  return SVN_NO_ERROR;
}

void callCloseTunnelCallback(JNIEnv* env, jobject jclosecb)
{
  static jmethodID mid = 0;
  if (0 == mid)
    {
      jclass cls;
      SVN_JNI_CATCH_VOID(
          cls= env->FindClass(
              JAVAHL_CLASS("/callback/TunnelAgent$CloseTunnelCallback")));
      SVN_JNI_CATCH_VOID(mid = env->GetMethodID(cls, "closeTunnel", "()V"));
    }
  SVN_JNI_CATCH_VOID(env->CallVoidMethod(jclosecb, mid));
  env->DeleteGlobalRef(jclosecb);
}

void
OperationContext::closeTunnel(void *tunnel_context, void *)
{
  TunnelContext* tc = static_cast<TunnelContext*>(tunnel_context);
  jobject jrequest = tc->jrequest;
  jobject jresponse = tc->jresponse;
  jobject jclosecb = tc->jclosecb;

  // Note that this closes other end of the pipe, which cancels and
  // prevents further read/writes in 'TunnelAgent'
  delete tc;

  JNIEnv *env = JNIUtil::getEnv();

  // Cleanup is important, otherwise TunnelAgent may crash when
  // accessing freed native objects. For this reason, cleanup is done
  // despite a pending exception. If more exceptions occur, they are
  // stashed as well in order to complete all cleanup steps.
  StashException ex(env);

  if (jclosecb)
    callCloseTunnelCallback(env, jclosecb);

  if (jrequest)
    {
      ex.stashException();
      close_TunnelChannel(env, jrequest);
    }

  if (jresponse)
    {
      ex.stashException();
      close_TunnelChannel(env, jresponse);
    }
}
