/*
 * Copyright (c) 2010-2015 Pivotal Software, Inc. All rights reserved.
 *
 * Licensed 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. See accompanying
 * LICENSE file.
 */

package com.pivotal.gemfirexd.thrift.server;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;

import org.apache.thrift.ProcessFunction;
import org.apache.thrift.TApplicationException;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TMessage;
import org.apache.thrift.protocol.TMessageType;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.protocol.TProtocolUtil;
import org.apache.thrift.protocol.TType;

import com.gemstone.gemfire.CancelCriterion;
import com.gemstone.gemfire.SystemFailure;
import com.gemstone.gemfire.cache.CacheClosedException;
import com.gemstone.gemfire.distributed.internal.ServerLocation;
import com.gemstone.gnu.trove.THashSet;
import com.pivotal.gemfirexd.internal.engine.GfxdConstants;
import com.pivotal.gemfirexd.internal.engine.Misc;
import com.pivotal.gemfirexd.internal.engine.distributed.utils.GemFireXDUtils;
import com.pivotal.gemfirexd.internal.engine.store.GemFireStore;
import com.pivotal.gemfirexd.internal.iapi.error.StandardException;
import com.pivotal.gemfirexd.internal.iapi.services.i18n.MessageService;
import com.pivotal.gemfirexd.internal.impl.jdbc.TransactionResourceImpl;
import com.pivotal.gemfirexd.internal.shared.common.error.ExceptionSeverity;
import com.pivotal.gemfirexd.internal.shared.common.reference.SQLState;
import com.pivotal.gemfirexd.internal.shared.common.sanity.SanityManager;
import com.pivotal.gemfirexd.thrift.GFXDException;
import com.pivotal.gemfirexd.thrift.GFXDExceptionData;
import com.pivotal.gemfirexd.thrift.GFXDService;
import com.pivotal.gemfirexd.thrift.HostAddress;
import com.pivotal.gemfirexd.thrift.LocatorService;
import com.pivotal.gemfirexd.thrift.ServerType;
import com.pivotal.gemfirexd.thrift.common.ThriftExceptionUtil;
import com.pivotal.gemfirexd.thrift.common.ThriftUtils;

/**
 * Server-side implementation of thrift LocatorService (see gfxd.thrift).
 * 
 * @author swale
 * @since gfxd 1.1
 */
public class LocatorServiceImpl implements LocatorService.Iface {

  protected final String hostAddress;
  protected final int hostPort;
  private volatile boolean isActive;
  private final CancelCriterion stopper;

  public LocatorServiceImpl(String address, int port) {
    this.hostAddress = address;
    this.hostPort = port;
    this.isActive = true;

    final GemFireStore store = Misc.getMemStoreBooting();
    this.stopper = new CancelCriterion() {

      @Override
      public RuntimeException generateCancelledException(Throwable t) {
        final RuntimeException ce;
        if ((ce = store.getAdvisee().getCancelCriterion()
            .generateCancelledException(t)) != null) {
          return ce;
        }
        return new CacheClosedException(MessageService.getCompleteMessage(
            SQLState.CLOUDSCAPE_SYSTEM_SHUTDOWN, null), t);
      }

      @Override
      public String cancelInProgress() {
        String cancel;
        if ((cancel = store.getAdvisee().getCancelCriterion()
            .cancelInProgress()) != null) {
          return cancel;
        }
        if (isActive()) {
          return null;
        }
        else {
          return MessageService.getCompleteMessage(
              SQLState.CLOUDSCAPE_SYSTEM_SHUTDOWN, null);
        }
      }
    };
  }

  /**
   * Get HostAddress of the preferred server w.r.t. load-balancing to connect to
   * from a thrift client. Client must use the returned server for the real data
   * connection to use the main {@link GFXDService.Iface} thrift API. A list of
   * servers to be excluded from consideration can be passed as a
   * comma-separated string (e.g. to ignore the failed server during failover).
   * <p>
   * If no server is available (after excluding the "failedServers"), then this
   * throws a GFXDException with SQLState "40XD2" (
   * {@link SQLState#DATA_CONTAINER_VANISHED}).
   */
  @Override
  public final HostAddress getPreferredServer(Set<ServerType> serverTypes,
      Set<String> serverGroups, Set<HostAddress> failedServers)
      throws GFXDException {

    if (failedServers == null) {
      failedServers = Collections.emptySet();
    }

    final Set<String> intersectGroups;
    final int ntypes;
    if (serverTypes != null && (ntypes = serverTypes.size()) > 0) {
      if (ntypes == 1) {
        intersectGroups = Collections.singleton(serverTypes.iterator().next()
            .getServerGroupName());
      }
      else {
        @SuppressWarnings("unchecked")
        Set<String> igroups = new THashSet(ntypes);
        intersectGroups = igroups;
        for (ServerType serverType : serverTypes) {
          intersectGroups.add(serverType.getServerGroupName());
        }
      }
    }
    else {
      intersectGroups = null;
    }
    if (SanityManager.TraceClientHA) {
      SanityManager.DEBUG_PRINT(SanityManager.TRACE_CLIENT_HA,
          "getPreferredServer(): getting preferred server for typeGroups="
              + intersectGroups + (serverGroups != null ? " serverGroups="
                  + serverGroups : ""));
    }

    ServerLocation prefServer;
    HostAddress prefHost;
    try {
      prefServer = GemFireXDUtils.getPreferredServer(serverGroups,
          intersectGroups, failedServers, null, true);
    } catch (Throwable t) {
      throw gfxdException(t);
    }
    if (prefServer != null) {
      prefHost = ThriftUtils.getHostAddress(prefServer.getHostName(),
          prefServer.getPort());
    }
    else {
      // for consistency though null here is okay in Thrift protocol
      prefHost = HostAddress.NULL_ADDRESS;
    }
    return prefHost;
  }

  /**
   * Get two results: a HostAddress containing the preferred server w.r.t.
   * load-balancing to connect to from a thrift client (like in
   * {@link #getPreferredServer(List)}), and all the thrift servers available in
   * the distributed system as HostAddresses. A list of servers to be excluded
   * from consideration can be passed as a comma-separated string (e.g. to
   * ignore the failed server during failover).
   * <p>
   * The returned list has the first element as the preferred server while the
   * remaining elements in the list are all the thrift servers available in the
   * distributed system.
   * <p>
   * This is primarily to avoid making two calls to the servers from the clients
   * during connection creation or failover.
   * <p>
   * If no server is available (after excluding the "failedServers"), then this
   * throws a GFXDException with SQLState "40XD2" (
   * {@link SQLState#DATA_CONTAINER_VANISHED}).
   */
  @Override
  public final List<HostAddress> getAllServersWithPreferredServer(
      Set<ServerType> serverTypes, Set<String> serverGroups,
      Set<HostAddress> failedServers) throws GFXDException {
    HostAddress prefServer = getPreferredServer(serverTypes, serverGroups,
        failedServers);
    ArrayList<HostAddress> prefAndAllServers = new ArrayList<HostAddress>();
    // null result in a list causes Thrift protocol exception
    if (prefServer == null) {
      prefServer = HostAddress.NULL_ADDRESS;
    }

    final Set<ServerType> allTypes;
    if (serverTypes == null || serverTypes.isEmpty()) {
      allTypes = null;
    }
    else {
      @SuppressWarnings("unchecked")
      Set<ServerType> types = new THashSet(serverTypes.size() * 2);
      allTypes = types;
      ServerType locatorType;

      allTypes.addAll(serverTypes);
      for (ServerType serverType : serverTypes) {
        if (serverType.isThriftGFXD()) {
          locatorType = serverType.getCorrespondingLocatorType();
          if (!serverTypes.contains(locatorType)) {
            allTypes.add(locatorType);
          }
        }
      }
    }
    prefAndAllServers.add(prefServer);
    try {
      GemFireXDUtils.getGfxdAdvisor().getAllThriftServers(allTypes,
          prefAndAllServers);
    } catch (Throwable t) {
      throw gfxdException(t);
    }
    if (SanityManager.TraceClientHA) {
      SanityManager.DEBUG_PRINT(SanityManager.TRACE_CLIENT_HA,
          "getAllServersWithPreferredServer(): returning preferred server "
              + "and all hosts " + prefAndAllServers);
    }
    return prefAndAllServers;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void closeConnection() {
    // nothing to be done here; only signals the processor to cleanly close
    // server-side socket
  }

  /**
   * Custom Processor implementation to handle closeConnection by closing
   * server-side connection cleanly.
   */
  public static final class Processor extends
      LocatorService.Processor<LocatorServiceImpl> {

    private final LocatorServiceImpl inst;
    private final HashMap<String, ProcessFunction<LocatorServiceImpl, ?>> fnMap;

    public Processor(LocatorServiceImpl inst) {
      super(inst);
      this.inst = inst;
      this.fnMap = new HashMap<String, ProcessFunction<LocatorServiceImpl, ?>>(
          super.getProcessMapView());
    }

    @Override
    public final boolean process(final TProtocol in, final TProtocol out)
        throws TException {
      final TMessage msg = in.readMessageBegin();
      final ProcessFunction<LocatorServiceImpl, ?> fn = this.fnMap
          .get(msg.name);
      if (fn != null) {
        fn.process(msg.seqid, in, out, this.inst);
        // terminate connection on receiving closeConnection
        // direct class comparison should be the fastest way
        return fn.getClass() != LocatorService.Processor.closeConnection.class;
      }
      else {
        TProtocolUtil.skip(in, TType.STRUCT);
        in.readMessageEnd();
        TApplicationException x = new TApplicationException(
            TApplicationException.UNKNOWN_METHOD, "Invalid method name: '"
                + msg.name + "'");
        out.writeMessageBegin(new TMessage(msg.name, TMessageType.EXCEPTION,
            msg.seqid));
        x.write(out);
        out.writeMessageEnd();
        out.getTransport().flush();
        return true;
      }
    }
  }

  public final CancelCriterion getCancelCriterion() {
    return this.stopper;
  }

  protected GFXDException gfxdException(Throwable t) {
    SQLException sqle;
    if (t instanceof SQLException) {
      sqle = (SQLException)t;
    }
    else if (t instanceof GFXDException) {
      return (GFXDException)t;
    }
    else {
      if (t instanceof Error) {
        Error err = (Error)t;
        if (SystemFailure.isJVMFailureError(err)) {
          SystemFailure.initiateFailure(err);
          // If this ever returns, rethrow the error. We're poisoned
          // now, so don't let this thread continue.
          throw err;
        }
        // Whenever you catch Error or Throwable, you must also
        // check for fatal JVM error (see above). However, there is
        // _still_ a possibility that you are dealing with a cascading
        // error condition, so you also need to check to see if the JVM
        // is still usable.
        SystemFailure.checkFailure();
        // If the above returns then send back error since this may be
        // assertion or some other internal code bug.
      }
      // check node going down
      String nodeFailure = getCancelCriterion().cancelInProgress();
      if (nodeFailure != null) {
        if (!GemFireXDUtils.nodeFailureException(t)) {
          t = getCancelCriterion().generateCancelledException(t);
        }
      }
      else {
        // print to server log
        log("Unexpected error in execution", t, null, true);
      }
      sqle = TransactionResourceImpl.wrapInSQLException(t);
    }

    GFXDExceptionData exData = new GFXDExceptionData(sqle.getMessage(),
        sqle.getSQLState(), sqle.getErrorCode());
    ArrayList<GFXDExceptionData> nextExceptions =
        new ArrayList<GFXDExceptionData>(4);
    SQLException next = sqle.getNextException();
    if (next != null) {
      nextExceptions = new ArrayList<GFXDExceptionData>();
      do {
        nextExceptions.add(new GFXDExceptionData(next.getMessage(), next
            .getSQLState(), next.getErrorCode()));
      } while ((next = next.getNextException()) != null);
    }
    GFXDException gfxde = new GFXDException(exData, getServerInfo());
    // append the server stack trace at the end
    final StringBuilder stack;
    if (t instanceof TException) {
      stack = new StringBuilder("Cause: ").append(
          ThriftExceptionUtil.getExceptionString(t)).append("; Server STACK: ");
    }
    else {
      stack = new StringBuilder("Server STACK: ");
    }
    SanityManager.getStackTrace(t, stack);
    nextExceptions.add(new GFXDExceptionData(stack.toString(),
        SQLState.GFXD_SERVER_STACK_INDICATOR,
        ExceptionSeverity.STATEMENT_SEVERITY));
    gfxde.setNextExceptions(nextExceptions);
    return gfxde;
  }

  public GFXDException newGFXDException(String messageId, Object... args) {
    GFXDExceptionData exData = new GFXDExceptionData();
    exData.setSqlState(StandardException.getSQLStateFromIdentifier(messageId));
    exData.setSeverity(StandardException.getSeverityFromIdentifier(messageId));
    exData.setReason(MessageService.getCompleteMessage(messageId, args));
    return new GFXDException(exData, getServerInfo());
  }

  protected String getServerInfo() {
    return "Locator=" + this.hostAddress + '[' + this.hostPort + "] Thread="
        + Thread.currentThread().getName();
  }

  static void log(final String message, Throwable t, String logLevel,
      boolean forceLog) {
    if (forceLog | GemFireXDUtils.TraceThriftAPI) {
      if (logLevel != null) {
        logLevel = logLevel + ':' + GfxdConstants.TRACE_THRIFT_API;
      }
      else {
        logLevel = GfxdConstants.TRACE_THRIFT_API;
      }
      SanityManager.DEBUG_PRINT(logLevel, message, t);
    }
  }

  public final boolean isActive() {
    return this.isActive;
  }

  public void stop() {
    this.isActive = false;
  }
}