/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <map>
#include <string>

#include <geode/ExceptionTypes.hpp>

#include "ErrType.hpp"
#include "util/Log.hpp"

namespace {

using apache::geode::client::AllConnectionsInUseException;
using apache::geode::client::AuthenticationFailedException;
using apache::geode::client::AuthenticationRequiredException;
using apache::geode::client::BufferSizeExceededException;
using apache::geode::client::CacheClosedException;
using apache::geode::client::CacheListenerException;
using apache::geode::client::CacheLoaderException;
using apache::geode::client::CacheProxyException;
using apache::geode::client::CacheServerException;
using apache::geode::client::CacheWriterException;
using apache::geode::client::CommitConflictException;
using apache::geode::client::ConcurrentModificationException;
using apache::geode::client::DiskFailureException;
using apache::geode::client::DuplicateDurableClientException;
using apache::geode::client::EntryDestroyedException;
using apache::geode::client::EntryExistsException;
using apache::geode::client::EntryNotFoundException;
using apache::geode::client::FunctionException;
using apache::geode::client::GeodeIOException;
using apache::geode::client::IllegalArgumentException;
using apache::geode::client::IllegalStateException;
using apache::geode::client::LeaseExpiredException;
using apache::geode::client::LowMemoryException;
using apache::geode::client::MessageException;
using apache::geode::client::NoAvailableLocatorsException;
using apache::geode::client::NoSystemException;
using apache::geode::client::NotAuthorizedException;
using apache::geode::client::NotConnectedException;
using apache::geode::client::NotOwnerException;
using apache::geode::client::OutOfMemoryException;
using apache::geode::client::PutAllPartialResultException;
using apache::geode::client::QueryException;
using apache::geode::client::QueryExecutionLowMemoryException;
using apache::geode::client::RegionDestroyedException;
using apache::geode::client::RegionExistsException;
using apache::geode::client::RollbackException;
using apache::geode::client::StatisticsDisabledException;
using apache::geode::client::TimeoutException;
using apache::geode::client::TransactionDataNodeHasDepartedException;
using apache::geode::client::TransactionDataRebalancedException;
using apache::geode::client::UnknownException;

[[noreturn]] void notConnectedException(std::string message, std::string& exMsg,
                                        GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": not connected to Geode");
  throw NotConnectedException{message};
}

[[noreturn]] void messageException(std::string message, std::string& exMsg,
                                   GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg
                                : ": message from server could not be handled");
  throw MessageException{message};
}

[[noreturn]] void cacheServerException(std::string message, std::string& exMsg,
                                       GfErrType err, std::string) {
  if (err == GF_CACHESERVER_EXCEPTION) {
    message.append(!exMsg.empty() ? exMsg
                                  : ": exception happened at cache server");
  }
  if (err == GF_CACHE_REGION_NOT_FOUND) {
    message.append(!exMsg.empty() ? exMsg : ": region not found on server");
  }

  throw CacheServerException{message};
}

[[noreturn]] void notOwnerException(std::string message, std::string& exMsg,
                                    GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": not own the lock");
  throw NotOwnerException{message};
}

[[noreturn]] void illegalStateException(std::string message, std::string& exMsg,
                                        GfErrType err, std::string) {
  if (err == GF_CACHE_REGION_NOT_GLOBAL) {
    message.append(!exMsg.empty() ? exMsg : ": region not global");
  }
  if (err == GF_CACHE_ILLEGAL_STATE_EXCEPTION) {
    message.append(!exMsg.empty() ? exMsg : ": illegal State");
  }
  throw IllegalStateException{message};
}

[[noreturn]] void illegalArgumentException(std::string message,
                                           std::string& exMsg, GfErrType err,
                                           std::string) {
  if (err == GF_CACHE_ILLEGAL_ARGUMENT_EXCEPTION) {
    message.append(!exMsg.empty() ? exMsg : ": illegal argument");
  }
  if (err == GF_CACHE_REGION_KEYS_NOT_STRINGS) {
    message.append(!exMsg.empty() ? exMsg
                                  : ": region entries do not support C access");
  }
  if (err == GF_CACHE_REGION_ENTRY_NOT_BYTES) {
    message.append(!exMsg.empty()
                       ? exMsg
                       : ": existing non-null values was not a byte array");
  }
  throw IllegalArgumentException{message};
}

[[noreturn]] void cacheWriterException(std::string message, std::string& exMsg,
                                       GfErrType err, std::string) {
  if (err == GF_CACHE_WRITER_EXCEPTION) {
    message.append(!exMsg.empty() ? exMsg
                                  : ": exception on server during write");
  }
  if (err == GF_CACHEWRITER_ERROR) {
    message.append(!exMsg.empty() ? exMsg : ": exception in CacheWriter");
  }
  throw CacheWriterException{message};
}

[[noreturn]] void cacheLoaderException(std::string message, std::string& exMsg,
                                       GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": exception in CacheLoader");
  throw CacheLoaderException{message};
}

[[noreturn]] void cacheListenerException(std::string message,
                                         std::string& exMsg, GfErrType,
                                         std::string) {
  message.append(!exMsg.empty() ? exMsg : ": exception in CacheListener");
  throw CacheListenerException{message};
}

[[noreturn]] void regionDestroyedException(std::string message,
                                           std::string& exMsg, GfErrType err,
                                           std::string) {
  if (err == GF_CACHE_REGION_INVALID) {
    message.append(!exMsg.empty() ? exMsg : ": region not valid");
  }
  if (err == GF_CACHE_REGION_DESTROYED_EXCEPTION) {
    message.append(!exMsg.empty() ? exMsg : ": Named Region Destroyed");
  }
  throw RegionDestroyedException{message};
}

[[noreturn]] void cacheProxyException(std::string message, std::string& exMsg,
                                      GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": error in Cache proxy");
  throw CacheProxyException{message};
}

[[noreturn]] void geodeIOException(std::string message, std::string& exMsg,
                                   GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Input/Output error in operation");
  throw GeodeIOException{message};
}

[[noreturn]] void noSystemException(std::string message, std::string& exMsg,
                                    GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": entity does not exist");
  throw NoSystemException{message};
}

[[noreturn]] void timeoutException(std::string message, std::string& exMsg,
                                   GfErrType err, std::string) {
  if (err == GF_CLIENT_WAIT_TIMEOUT) {
    message.append(!exMsg.empty()
                       ? exMsg
                       : ": timed out, possibly bucket is not available.");
  } else {
    message.append(!exMsg.empty() ? exMsg : ": timed out");
  }
  throw TimeoutException{message};
}

[[noreturn]] void outOfMemoryException(std::string message, std::string& exMsg,
                                       GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Out of memory");
  throw OutOfMemoryException{message};
}

[[noreturn]] void bufferSizeExceededException(std::string message,
                                              std::string& exMsg, GfErrType,
                                              std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Buffer Size Exceeded");
  throw BufferSizeExceededException{message};
}

[[noreturn]] void leaseExpiredException(std::string message, std::string& exMsg,
                                        GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": lock Lease Expired On you");
  throw LeaseExpiredException{message};
}

[[noreturn]] void regionExistsException(std::string message, std::string& exMsg,
                                        GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Named Region Exists");
  throw RegionExistsException{message};
}

[[noreturn]] void entryNotFoundException(std::string message,
                                         std::string& exMsg, GfErrType,
                                         std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Entry not found");
  throw EntryNotFoundException{message};
}

[[noreturn]] void entryExistsException(std::string message, std::string& exMsg,
                                       GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg
                                : ": Entry already exists in the region");
  throw EntryExistsException{message};
}

[[noreturn]] void entryDestroyedException(std::string message,
                                          std::string& exMsg, GfErrType,
                                          std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Entry has been destroyed");
  throw EntryDestroyedException{message};
}

[[noreturn]] void cacheClosedException(std::string message, std::string& exMsg,
                                       GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Cache has been closed");
  throw CacheClosedException{message};
}

[[noreturn]] void statisticsDisabledException(std::string message,
                                              std::string& exMsg, GfErrType,
                                              std::string) {
  message.append(!exMsg.empty()
                     ? exMsg
                     : ": Statistics have been disabled for the region");
  throw StatisticsDisabledException{message};
}

[[noreturn]] void concurrentModificationException(std::string message,
                                                  std::string& exMsg, GfErrType,
                                                  std::string) {
  message.append(!exMsg.empty() ? exMsg
                                : ": Concurrent modification in the cache");
  throw ConcurrentModificationException{message};
}

[[noreturn]] void notAuthorizedException(std::string message,
                                         std::string& exMsg, GfErrType,
                                         std::string) {
  message.append(!exMsg.empty() ? exMsg : ": unauthorized operation");
  throw NotAuthorizedException{message};
}

[[noreturn]] void authenticationFailedException(std::string message,
                                                std::string& exMsg, GfErrType,
                                                std::string) {
  message.append(!exMsg.empty() ? exMsg : ": authentication failed");
  throw AuthenticationFailedException{message};
}

[[noreturn]] void authenticationRequiredException(std::string message,
                                                  std::string& exMsg, GfErrType,
                                                  std::string) {
  message.append(!exMsg.empty() ? exMsg : ": no authentication provided");
  throw AuthenticationRequiredException{message};
}

[[noreturn]] void duplicateDurableClientException(std::string message,
                                                  std::string& exMsg, GfErrType,
                                                  std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Duplicate Durable Client Id");
  throw DuplicateDurableClientException{message};
}

[[noreturn]] void queryException(std::string message, std::string& exMsg,
                                 GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Query failed");
  throw QueryException{message};
}

[[noreturn]] void noAvailableLocatorsException(std::string, std::string& exMsg,
                                               GfErrType, std::string func) {
  try {
    throw NoAvailableLocatorsException{
        func + (!exMsg.empty() ? exMsg : ": No locators available")};
  } catch (...) {
    std::throw_with_nested(NotConnectedException{
        func + (!exMsg.empty() ? exMsg : ": No locators available")});
  }
}

[[noreturn]] void allConnectionsInUseException(std::string message,
                                               std::string& exMsg, GfErrType,
                                               std::string) {
  message.append(!exMsg.empty() ? exMsg : ": All connections are in use");
  throw AllConnectionsInUseException{message};
}

[[noreturn]] void functionException(std::string message,
                                    const std::string& exMsg, GfErrType,
                                    std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Function execution failed");
  throw FunctionException{message};
}

[[noreturn]] void diskFailureException(std::string message, std::string& exMsg,
                                       GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Disk full");
  throw DiskFailureException{message};
}

[[noreturn]] void rollbackException(std::string message, std::string& exMsg,
                                    GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Transaction rolled back");
  throw RollbackException{message};
}

[[noreturn]] void commitConflictException(std::string message,
                                          std::string& exMsg, GfErrType,
                                          std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Commit conflict exception");
  throw CommitConflictException{message};
}

[[noreturn]] void transactionDataRebalancedException(std::string message,
                                                     std::string& exMsg,
                                                     GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg
                                : ": Transaction data rebalanced exception");
  throw TransactionDataRebalancedException{message};
}

[[noreturn]] void transactionDataNodeHasDepartedException(std::string message,
                                                          std::string& exMsg,
                                                          GfErrType,
                                                          std::string) {
  message.append(!exMsg.empty()
                     ? exMsg
                     : ": Transaction data node has departed exception");
  throw TransactionDataNodeHasDepartedException{message};
}

[[noreturn]] void putAllPartialResultException(std::string message,
                                               std::string& exMsg, GfErrType,
                                               std::string) {
  message.append(!exMsg.empty() ? exMsg : ": PutAll Partial exception");
  throw PutAllPartialResultException{message};
}

[[noreturn]] void lowMemoryException(std::string message, std::string& exMsg,
                                     GfErrType, std::string) {
  message.append(!exMsg.empty() ? exMsg : ": Low memory exception");
  throw LowMemoryException{message};
}

[[noreturn]] void queryLowMemoryException(std::string message,
                                          std::string& exMsg, GfErrType,
                                          std::string) {
  message.append(!exMsg.empty() ? exMsg
                                : ": Query execution low memory exception");
  throw QueryExecutionLowMemoryException{message};
}

[[noreturn]] void unknownException(std::string message, std::string& exMsg,
                                   GfErrType err, std::string func) {
  LOGINFO("error code: %d", err);
  message = func;
  if (exMsg.empty()) {
    message.append("Unknown error code ").append(std::to_string(err));
  } else {
    message.append(exMsg);
  }
  throw UnknownException{message};
}

}  // namespace

using error_function_t =
    std::function<void(std::string, std::string&, GfErrType, std::string)>;

std::map<GfErrType, error_function_t>& get_error_map() {
  static std::map<GfErrType, error_function_t> error_map{
      {GF_NOTCON, notConnectedException},
      {GF_MSG, messageException},
      {GF_CACHESERVER_EXCEPTION, cacheServerException},
      {GF_NOTOWN, notOwnerException},
      {GF_CACHE_REGION_NOT_FOUND, cacheServerException},
      {GF_CACHE_REGION_NOT_GLOBAL, illegalStateException},
      {GF_CACHE_ILLEGAL_ARGUMENT_EXCEPTION, illegalArgumentException},
      {GF_CACHE_ILLEGAL_STATE_EXCEPTION, illegalStateException},
      {GF_CACHE_WRITER_EXCEPTION, cacheWriterException},
      {GF_CACHEWRITER_ERROR, cacheWriterException},
      {GF_CACHE_LOADER_EXCEPTION, cacheLoaderException},
      {GF_CACHE_LISTENER_EXCEPTION, cacheListenerException},
      {GF_CACHE_REGION_INVALID, regionDestroyedException},
      {GF_CACHE_PROXY, cacheProxyException},
      {GF_IOERR, geodeIOException},
      {GF_ENOENT, noSystemException},
      {GF_CACHE_REGION_KEYS_NOT_STRINGS, illegalArgumentException},
      {GF_CACHE_REGION_ENTRY_NOT_BYTES, illegalArgumentException},
      {GF_CACHE_TIMEOUT_EXCEPTION, timeoutException},
      {GF_TIMEOUT, timeoutException},
      {GF_CLIENT_WAIT_TIMEOUT, timeoutException},
      {GF_ENOMEM, outOfMemoryException},
      {GF_ERANGE, bufferSizeExceededException},
      {GF_CACHE_LEASE_EXPIRED_EXCEPTION, leaseExpiredException},
      {GF_CACHE_REGION_EXISTS_EXCEPTION, regionExistsException},
      {GF_CACHE_ENTRY_NOT_FOUND, entryNotFoundException},
      {GF_CACHE_ENTRY_EXISTS, entryExistsException},
      {GF_CACHE_ENTRY_DESTROYED_EXCEPTION, entryDestroyedException},
      {GF_CACHE_REGION_DESTROYED_EXCEPTION, regionDestroyedException},
      {GF_CACHE_CLOSED_EXCEPTION, cacheClosedException},
      {GF_CACHE_STATISTICS_DISABLED_EXCEPTION, statisticsDisabledException},
      {GF_CACHE_CONCURRENT_MODIFICATION_EXCEPTION,
       concurrentModificationException},
      {GF_NOT_AUTHORIZED_EXCEPTION, notAuthorizedException},
      {GF_AUTHENTICATION_FAILED_EXCEPTION, authenticationFailedException},
      {GF_AUTHENTICATION_REQUIRED_EXCEPTION, authenticationRequiredException},
      {GF_DUPLICATE_DURABLE_CLIENT, duplicateDurableClientException},
      {GF_REMOTE_QUERY_EXCEPTION, queryException},
      {GF_CACHE_LOCATOR_EXCEPTION, noAvailableLocatorsException},
      {GF_ALL_CONNECTIONS_IN_USE_EXCEPTION, allConnectionsInUseException},
      {GF_INTERNAL_FUNCTION_INVOCATION_TARGET_EXCEPTION, functionException},
      {GF_FUNCTION_EXCEPTION, functionException},
      {GF_DISKFULL, diskFailureException},
      {GF_ROLLBACK_EXCEPTION, rollbackException},
      {GF_COMMIT_CONFLICT_EXCEPTION, commitConflictException},
      {GF_TRANSACTION_DATA_REBALANCED_EXCEPTION,
       transactionDataRebalancedException},
      {GF_TRANSACTION_DATA_NODE_HAS_DEPARTED_EXCEPTION,
       transactionDataNodeHasDepartedException},
      {GF_PUTALL_PARTIAL_RESULT_EXCEPTION, putAllPartialResultException},
      {GF_LOW_MEMORY_EXCEPTION, lowMemoryException},
      {GF_QUERY_EXECUTION_LOW_MEMORY_EXCEPTION, queryLowMemoryException},
      {GF_NOERR, unknownException},
      {GF_DEADLK, unknownException},
      {GF_EACCES, unknownException},
      {GF_ECONFL, unknownException},
      {GF_EINVAL, unknownException},
      {GF_ETYPE, unknownException},
      {GF_NOTOBJ, unknownException},
      {GF_NOTSUP, unknownException},
      {GF_SCPGBL, unknownException},
      {GF_SCPEXC, unknownException},
      {GF_OVRFLW, unknownException},
      {GF_EINTR, unknownException},
      {GF_NOSERVER_FOUND, unknownException},
      {GF_SERVER_FAILED, unknownException},
      {GF_CLIENT_WAIT_TIMEOUT_REFRESH_PRMETADATA, unknownException},
      {GF_CANNOT_PROCESS_GII_REQUEST, unknownException},
      {GF_CACHE_ENTRY_UPDATED, unknownException},
      {GF_INVALID_DELTA, unknownException},
      {GF_EUNDEF, unknownException}};

  return error_map;
}

namespace apache {
namespace geode {
namespace client {

void setThreadLocalExceptionMessage(std::string exMsg);
const std::string& getThreadLocalExceptionMessage();

[[noreturn]] void GfErrTypeThrowException(const char* str, GfErrType err) {
  std::string func;
  std::string message;
  auto exMsg = getThreadLocalExceptionMessage();
  setThreadLocalExceptionMessage("");
  if (!exMsg.empty()) {
    func.append(str);
    func.append(": ");
  }

  auto& error_map = get_error_map();
  auto iter = error_map.find(err);
  if (iter != std::end(error_map)) {
    iter->second(message, exMsg, err, func);
  }
  unknownException(message, exMsg, err, func);
}
}  // namespace client
}  // namespace geode
}  // namespace apache
