/*
 * 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.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.logging.log4j.Logger;

import org.apache.geode.cache.Region;
import org.apache.geode.cache.client.ServerConnectivityException;
import org.apache.geode.cache.client.ServerOperationException;
import org.apache.geode.distributed.internal.ServerLocation;
import org.apache.geode.internal.Version;
import org.apache.geode.internal.cache.LocalRegion;
import org.apache.geode.internal.cache.tier.MessageType;
import org.apache.geode.internal.cache.tier.sockets.ChunkedMessage;
import org.apache.geode.internal.cache.tier.sockets.Message;
import org.apache.geode.internal.cache.tier.sockets.Part;
import org.apache.geode.internal.cache.tier.sockets.VersionedObjectList;
import org.apache.geode.internal.logging.LogService;

/**
 * Does a region getAll on a server
 *
 * @since GemFire 5.7
 */
public class GetAllOp {

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

  /**
   * Does a region getAll on a server using connections from the given pool to communicate with the
   * server.
   *
   * @param pool the pool to use to communicate with the server.
   * @param region the name of the region to do the getAll on
   * @param keys list of keys to get
   * @return the map of values found by the getAll if any
   */
  public static VersionedObjectList execute(ExecutablePool pool, String region, List keys,
      Object callback) {
    AbstractOp op = new GetAllOpImpl(region, keys, callback);
    op.initMessagePart();
    return ((VersionedObjectList) pool.execute(op)).setKeys(keys);
  }

  public static VersionedObjectList execute(ExecutablePool pool, Region region, List keys,
      int retryAttempts, Object callback) {
    AbstractOp op = new GetAllOpImpl(region.getFullPath(), keys, callback);
    ClientMetadataService cms = ((LocalRegion) region).getCache().getClientMetadataService();

    Map<ServerLocation, HashSet> serverToFilterMap = cms.getServerToFilterMap(keys, region, true);

    if (serverToFilterMap == null || serverToFilterMap.isEmpty()) {
      op.initMessagePart();
      return ((VersionedObjectList) pool.execute(op)).setKeys(keys);
    } else {
      VersionedObjectList result = null;
      ServerConnectivityException se = null;
      List retryList = new ArrayList();
      List callableTasks =
          constructGetAllTasks(region.getFullPath(), serverToFilterMap, (PoolImpl) pool, callback);
      Map<ServerLocation, Object> results = SingleHopClientExecutor.submitGetAll(serverToFilterMap,
          callableTasks, cms, (LocalRegion) region);
      for (ServerLocation server : results.keySet()) {
        Object serverResult = results.get(server);
        if (serverResult instanceof ServerConnectivityException) {
          se = (ServerConnectivityException) serverResult;
          retryList.addAll(serverToFilterMap.get(server));
        } else {
          if (result == null) {
            result = (VersionedObjectList) serverResult;
          } else {
            result.addAll((VersionedObjectList) serverResult);
          }
        }
      }

      if (se != null) {
        if (retryAttempts == 0) {
          throw se;
        } else {
          VersionedObjectList retryResult =
              GetAllOp.execute(pool, region.getFullPath(), retryList, callback);
          if (result == null) {
            result = retryResult;
          } else {
            result.addAll(retryResult);
          }
        }
      }

      return result;
    }
  }

  private GetAllOp() {
    // no instances allowed
  }

  static List constructGetAllTasks(String region,
      final Map<ServerLocation, HashSet> serverToFilterMap, final PoolImpl pool,
      final Object callback) {
    final List<SingleHopOperationCallable> tasks = new ArrayList<SingleHopOperationCallable>();
    ArrayList<ServerLocation> servers = new ArrayList<ServerLocation>(serverToFilterMap.keySet());

    if (logger.isDebugEnabled()) {
      logger.debug("Constructing tasks for the servers {}", servers);
    }
    for (ServerLocation server : servers) {
      Set filterSet = serverToFilterMap.get(server);
      AbstractOp getAllOp = new GetAllOpImpl(region, new ArrayList(filterSet), callback);

      SingleHopOperationCallable task =
          new SingleHopOperationCallable(new ServerLocation(server.getHostName(), server.getPort()),
              pool, getAllOp, UserAttributes.userAttributes.get());
      tasks.add(task);
    }
    return tasks;
  }

  static class GetAllOpImpl extends AbstractOp {

    private List keyList;
    private final Object callback;

    /**
     * @throws org.apache.geode.SerializationException if serialization fails
     */
    public GetAllOpImpl(String region, List keys, Object callback) {
      super(callback != null ? MessageType.GET_ALL_WITH_CALLBACK : MessageType.GET_ALL_70, 3);
      this.keyList = keys;
      this.callback = callback;
      getMessage().addStringPart(region);
    }

    @Override
    protected void initMessagePart() {
      Object[] keysArray = new Object[this.keyList.size()];
      this.keyList.toArray(keysArray);
      getMessage().addObjPart(keysArray);
      if (this.callback != null) {
        getMessage().addObjPart(this.callback);
      } else {
        // using the old GET_ALL_70 command that expects an int saying we are not register interest
        getMessage().addIntPart(0);
      }
    }

    public List getKeyList() {
      return this.keyList;
    }


    @Override
    protected Message createResponseMessage() {
      return new ChunkedMessage(1, Version.CURRENT);
    }

    @Override
    protected Object processResponse(Message msg) throws Exception {
      throw new UnsupportedOperationException();
    }

    @Override
    protected Object processResponse(Message msg, final Connection con) throws Exception {
      final VersionedObjectList result = new VersionedObjectList(false);
      final Exception[] exceptionRef = new Exception[1];
      processChunkedResponse((ChunkedMessage) msg, "getAll", new ChunkHandler() {
        public void handle(ChunkedMessage cm) throws Exception {
          Part part = cm.getPart(0);
          try {
            Object o = part.getObject();
            if (o instanceof Throwable) {
              String s = "While performing a remote getAll";
              exceptionRef[0] = new ServerOperationException(s, (Throwable) o);
            } else {
              VersionedObjectList chunk = (VersionedObjectList) o;
              chunk.replaceNullIDs(con.getEndpoint().getMemberId());
              result.addAll(chunk);
            }
          } catch (Exception e) {
            exceptionRef[0] = new ServerOperationException("Unable to deserialize value", e);
          }
        }
      });
      if (exceptionRef[0] != null) {
        throw exceptionRef[0];
      } else {
        return result;
      }
    }

    @Override
    protected boolean isErrorResponse(int msgType) {
      return msgType == MessageType.GET_ALL_DATA_ERROR;
    }

    @Override
    protected long startAttempt(ConnectionStats stats) {
      return stats.startGetAll();
    }

    @Override
    protected void endSendAttempt(ConnectionStats stats, long start) {
      stats.endGetAllSend(start, hasFailed());
    }

    @Override
    protected void endAttempt(ConnectionStats stats, long start) {
      stats.endGetAll(start, hasTimedOut(), hasFailed());
    }
  }
}
