/*
 * 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.
 */
package org.apache.geode.cache.client.internal;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.logging.log4j.Logger;

import org.apache.geode.GemFireException;
import org.apache.geode.InternalGemFireException;
import org.apache.geode.cache.CacheClosedException;
import org.apache.geode.cache.client.ServerConnectivityException;
import org.apache.geode.cache.client.ServerOperationException;
import org.apache.geode.cache.client.internal.GetAllOp.GetAllOpImpl;
import org.apache.geode.cache.execute.FunctionException;
import org.apache.geode.cache.execute.FunctionInvocationTargetException;
import org.apache.geode.cache.execute.ResultCollector;
import org.apache.geode.distributed.internal.ServerLocation;
import org.apache.geode.internal.cache.LocalRegion;
import org.apache.geode.internal.cache.PutAllPartialResultException;
import org.apache.geode.internal.cache.execute.BucketMovedException;
import org.apache.geode.internal.cache.execute.InternalFunctionInvocationTargetException;
import org.apache.geode.internal.cache.tier.sockets.VersionedObjectList;
import org.apache.geode.internal.i18n.LocalizedStrings;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.internal.logging.LoggingThreadGroup;
import org.apache.geode.internal.logging.log4j.LocalizedMessage;

public class SingleHopClientExecutor {

  private static final Logger logger = LogService.getLogger();

  static final ExecutorService execService = Executors.newCachedThreadPool(new ThreadFactory() {
    AtomicInteger threadNum = new AtomicInteger();

    public Thread newThread(final Runnable r) {
      Thread result =
          new Thread(LoggingThreadGroup.createThreadGroup("FunctionExecutionThreadGroup", logger),
              r, "Function Execution Thread-" + threadNum.incrementAndGet());
      result.setDaemon(true);
      return result;
    }
  });

  static void submitAll(List callableTasks) {
    if (callableTasks != null && !callableTasks.isEmpty()) {
      List futures = null;
      try {
        futures = execService.invokeAll(callableTasks);
      } catch (InterruptedException e) {
        throw new InternalGemFireException(e.getMessage());
      }
      if (futures != null) {
        Iterator itr = futures.iterator();
        while (itr.hasNext() && !execService.isShutdown() && !execService.isTerminated()) {
          Future fut = (Future) itr.next();
          try {
            fut.get();
          } catch (InterruptedException e) {
            throw new InternalGemFireException(e.getMessage());
          } catch (ExecutionException ee) {
            if (ee.getCause() instanceof FunctionException) {
              throw (FunctionException) ee.getCause();
            } else if (ee.getCause() instanceof ServerOperationException) {
              throw (ServerOperationException) ee.getCause();
            } else if (ee.getCause() instanceof ServerConnectivityException) {
              throw (ServerConnectivityException) ee.getCause();
            } else {
              throw executionThrowable(ee.getCause());
            }
          }
        }
      }
    }
  }

  static boolean submitAllHA(List callableTasks, LocalRegion region, boolean isHA,
      ResultCollector rc, Set<String> failedNodes) {

    ClientMetadataService cms = region.getCache().getClientMetadataService();
    boolean reexecute = false;

    if (callableTasks != null && !callableTasks.isEmpty()) {
      List futures = null;
      try {
        futures = execService.invokeAll(callableTasks);
      } catch (InterruptedException e) {
        throw new InternalGemFireException(e.getMessage());
      }
      if (futures != null) {
        GemFireException functionExecutionException = null;
        Iterator futureItr = futures.iterator();
        Iterator taskItr = callableTasks.iterator();
        final boolean isDebugEnabled = logger.isDebugEnabled();
        while (futureItr.hasNext() && !execService.isShutdown() && !execService.isTerminated()) {
          Future fut = (Future) futureItr.next();
          SingleHopOperationCallable task = (SingleHopOperationCallable) taskItr.next();
          ServerLocation server = task.getServer();
          try {
            fut.get();
            if (isDebugEnabled) {
              logger.debug("ExecuteRegionFunctionSingleHopOp#got result from {}", server);
            }
          } catch (InterruptedException e) {
            throw new InternalGemFireException(e.getMessage());
          } catch (ExecutionException ee) {
            if (ee.getCause() instanceof InternalFunctionInvocationTargetException) {
              if (isDebugEnabled) {
                logger.debug(
                    "ExecuteRegionFunctionSingleHopOp#ExecutionException.InternalFunctionInvocationTargetException : Caused by :{}",
                    ee.getCause());
              }
              try {
                cms = region.getCache().getClientMetadataService();
              } catch (CacheClosedException e) {
                return false;
              }
              cms.removeBucketServerLocation(server);
              cms.scheduleGetPRMetaData(region, false);
              reexecute = true;
              failedNodes.addAll(
                  ((InternalFunctionInvocationTargetException) ee.getCause()).getFailedNodeSet());
              // Clear the results only if isHA so that partial results can be returned.
              if (isHA) {
                rc.clearResults();
              } else {
                if (ee.getCause().getCause() != null) {
                  functionExecutionException =
                      new FunctionInvocationTargetException(ee.getCause().getCause());
                } else {
                  functionExecutionException =
                      new FunctionInvocationTargetException(new BucketMovedException(
                          LocalizedStrings.FunctionService_BUCKET_MIGRATED_TO_ANOTHER_NODE
                              .toLocalizedString()));
                }
              }
            } else if (ee.getCause() instanceof FunctionException) {
              if (isDebugEnabled) {
                logger.debug(
                    "ExecuteRegionFunctionSingleHopOp#ExecutionException.FunctionException : Caused by :{}",
                    ee.getCause());
              }
              FunctionException fe = (FunctionException) ee.getCause();
              if (isHA) {
                throw fe;
              } else {
                functionExecutionException = fe;
              }
            } else if (ee.getCause() instanceof ServerOperationException) {
              if (isDebugEnabled) {
                logger.debug(
                    "ExecuteRegionFunctionSingleHopOp#ExecutionException.ServerOperationException : Caused by :{}",
                    ee.getCause());
              }
              ServerOperationException soe = (ServerOperationException) ee.getCause();
              if (isHA) {
                throw soe;
              } else {
                functionExecutionException = soe;
              }
            } else if (ee.getCause() instanceof ServerConnectivityException) {
              if (isDebugEnabled) {
                logger.debug(
                    "ExecuteRegionFunctionSingleHopOp#ExecutionException.ServerConnectivityException : Caused by :{} The failed server is: {}",
                    ee.getCause(), server);
              }
              try {
                cms = region.getCache().getClientMetadataService();
              } catch (CacheClosedException e) {
                return false;
              }
              cms.removeBucketServerLocation(server);
              cms.scheduleGetPRMetaData(region, false);
              // Clear the results only if isHA so that partial results can be returned.
              if (isHA) {
                reexecute = true;
                rc.clearResults();
              } else {
                functionExecutionException = (ServerConnectivityException) ee.getCause();
              }
            } else {
              throw executionThrowable(ee.getCause());
            }
          }
        }
        if (functionExecutionException != null) {
          throw functionExecutionException;
        }
      }
    }
    return reexecute;
  }

  /**
   * execute bulk op (putAll or removeAll) on multiple PR servers, returning a map of the results.
   * Results are either a VersionedObjectList or a BulkOpPartialResultsException
   *
   * @return the per-server results
   */
  static Map<ServerLocation, Object> submitBulkOp(List callableTasks, ClientMetadataService cms,
      LocalRegion region, Map<ServerLocation, RuntimeException> failedServers) {
    if (callableTasks != null && !callableTasks.isEmpty()) {
      Map<ServerLocation, Object> resultMap = new HashMap<ServerLocation, Object>();
      boolean anyPartialResults = false;
      List futures = null;
      try {
        futures = execService.invokeAll(callableTasks);
      } catch (InterruptedException e) {
        throw new InternalGemFireException(e.getMessage());
      }
      if (futures != null) {
        Iterator futureItr = futures.iterator();
        Iterator taskItr = callableTasks.iterator();
        RuntimeException rte = null;
        final boolean isDebugEnabled = logger.isDebugEnabled();
        while (futureItr.hasNext() && !execService.isShutdown() && !execService.isTerminated()) {
          Future fut = (Future) futureItr.next();
          SingleHopOperationCallable task = (SingleHopOperationCallable) taskItr.next();
          ServerLocation server = task.getServer();
          try {
            VersionedObjectList versions = (VersionedObjectList) fut.get();
            if (logger.isDebugEnabled()) {
              logger.debug("submitBulkOp#got result from {}:{}", server, versions);
            }
            resultMap.put(server, versions);
          } catch (InterruptedException e) {
            InternalGemFireException ige = new InternalGemFireException(e);
            // only to make this server as failed server, not to throw right now
            failedServers.put(server, ige);
            if (rte == null) {
              rte = ige;
            }
          } catch (ExecutionException ee) {
            if (ee.getCause() instanceof ServerOperationException) {
              if (logger.isDebugEnabled()) {
                logger.debug("submitBulkOp#ExecutionException from server {}", server, ee);
              }
              ServerOperationException soe = (ServerOperationException) ee.getCause();
              // only to make this server as failed server, not to throw right now
              failedServers.put(server, soe);
              if (rte == null) {
                rte = soe;
              }
            } else if (ee.getCause() instanceof ServerConnectivityException) {
              if (logger.isDebugEnabled()) {
                logger.debug("submitBulkOp#ExecutionException for server {}", server, ee);
              }
              cms = region.getCache().getClientMetadataService();
              cms.removeBucketServerLocation(server);
              cms.scheduleGetPRMetaData(region, false);
              failedServers.put(server, (ServerConnectivityException) ee.getCause());
            } else {
              Throwable t = ee.getCause();
              if (t instanceof PutAllPartialResultException) {
                resultMap.put(server, t);
                anyPartialResults = true;
                failedServers.put(server, (PutAllPartialResultException) t);
              } else {
                RuntimeException other_rte = executionThrowable(ee.getCause());
                failedServers.put(server, other_rte);
                if (rte == null) {
                  rte = other_rte;
                }
              }
            }
          } // catch
        } // while
        // if there are any partial results we suppress throwing an exception
        // so the partial results can be processed
        if (rte != null && !anyPartialResults) {
          throw rte;
        }
      }
      return resultMap;
    }
    return null;
  }

  static Map<ServerLocation, Object> submitGetAll(Map<ServerLocation, HashSet> serverToFilterMap,
      List callableTasks, ClientMetadataService cms, LocalRegion region) {

    if (callableTasks != null && !callableTasks.isEmpty()) {
      Map<ServerLocation, Object> resultMap = new HashMap<ServerLocation, Object>();
      List futures = null;
      try {
        futures = execService.invokeAll(callableTasks);
      } catch (InterruptedException e) {
        throw new InternalGemFireException(e.getMessage());
      }
      if (futures != null) {
        Iterator futureItr = futures.iterator();
        Iterator taskItr = callableTasks.iterator();
        while (futureItr.hasNext() && !execService.isShutdown() && !execService.isTerminated()) {
          Future fut = (Future) futureItr.next();
          SingleHopOperationCallable task = (SingleHopOperationCallable) taskItr.next();
          List keys = ((GetAllOpImpl) task.getOperation()).getKeyList();
          ServerLocation server = task.getServer();
          try {

            VersionedObjectList valuesFromServer = (VersionedObjectList) fut.get();
            valuesFromServer.setKeys(keys);

            for (VersionedObjectList.Iterator it = valuesFromServer.iterator(); it.hasNext();) {
              VersionedObjectList.Entry entry = it.next();
              Object key = entry.getKey();
              Object value = entry.getValue();
              if (!entry.isKeyNotOnServer()) {
                if (value instanceof Throwable) {
                  logger.warn(LocalizedMessage.create(
                      LocalizedStrings.GetAll_0_CAUGHT_THE_FOLLOWING_EXCEPTION_ATTEMPTING_TO_GET_VALUE_FOR_KEY_1,
                      new Object[] {value, key}), (Throwable) value);
                }
              }
            }
            if (logger.isDebugEnabled()) {
              logger.debug("GetAllOp#got result from {}: {}", server, valuesFromServer);
            }
            resultMap.put(server, valuesFromServer);
          } catch (InterruptedException e) {
            throw new InternalGemFireException(e.getMessage());
          } catch (ExecutionException ee) {
            if (ee.getCause() instanceof ServerOperationException) {
              if (logger.isDebugEnabled()) {
                logger.debug("GetAllOp#ExecutionException.ServerOperationException : Caused by :{}",
                    ee.getCause());
              }
              throw (ServerOperationException) ee.getCause();
            } else if (ee.getCause() instanceof ServerConnectivityException) {
              if (logger.isDebugEnabled()) {
                logger.debug(
                    "GetAllOp#ExecutionException.ServerConnectivityException : Caused by :{} The failed server is: {}",
                    ee.getCause(), server);
              }
              try {
                cms = region.getCache().getClientMetadataService();
              } catch (CacheClosedException e) {
                return null;
              }
              cms.removeBucketServerLocation(server);
              cms.scheduleGetPRMetaData((LocalRegion) region, false);
              resultMap.put(server, ee.getCause());
            } else {
              throw executionThrowable(ee.getCause());
            }
          }
        }
        return resultMap;
      }
    }
    return null;
  }

  static void submitTask(Runnable task) {
    execService.execute(task);
  }

  // Find out what exception to throw?
  private static RuntimeException executionThrowable(Throwable t) {
    if (t instanceof RuntimeException)
      return (RuntimeException) t;
    else if (t instanceof Error)
      throw (Error) t;
    else
      throw new IllegalStateException("Don't know", t);
  }
}
