/*
 * 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 com.datastax.driver.core;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;

import com.datastax.driver.core.policies.ReconnectionPolicy;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * The policy with which to decide whether a host should be considered down.
 *
 * <p>TODO: this class is fully abstract (rather than an interface) because I'm not sure it's worth
 * exposing (and if we do expose it, we need to expose ConnectionException). Maybe just exposing say
 * a threshold of error before convicting a node is enough.
 */
abstract class ConvictionPolicy {

  /**
   * Called when new connections to the host are about to be created.
   *
   * @param count the number of connections
   */
  abstract void signalConnectionsOpening(int count);

  /** Called when a connection closed normally. */
  abstract void signalConnectionClosed(Connection connection);

  /**
   * Called when a connection error occurs on a connection to the host this policy applies to.
   *
   * @return whether the host should be considered down.
   */
  abstract boolean signalConnectionFailure(Connection connection, boolean decrement);

  abstract boolean canReconnectNow();

  abstract boolean hasActiveConnections();

  /** Simple factory interface to allow creating {@link ConvictionPolicy} instances. */
  interface Factory {

    /**
     * Creates a new ConvictionPolicy instance for {@code host}.
     *
     * @param host the host this policy applies to
     * @return the newly created {@link ConvictionPolicy} instance.
     */
    ConvictionPolicy create(Host host, ReconnectionPolicy reconnectionPolicy);
  }

  static class DefaultConvictionPolicy extends ConvictionPolicy {
    private final Host host;
    private final ReconnectionPolicy reconnectionPolicy;
    private final AtomicInteger openConnections = new AtomicInteger();

    private volatile long nextReconnectionTime = Long.MIN_VALUE;
    private ReconnectionPolicy.ReconnectionSchedule reconnectionSchedule;

    private DefaultConvictionPolicy(Host host, ReconnectionPolicy reconnectionPolicy) {
      this.host = host;
      this.reconnectionPolicy = reconnectionPolicy;
    }

    @Override
    void signalConnectionsOpening(int count) {
      int newTotal = openConnections.addAndGet(count);
      Host.statesLogger.debug(
          "[{}] preparing to open {} new connections, total = {}", host, count, newTotal);
      resetReconnectionTime();
    }

    @Override
    void signalConnectionClosed(Connection connection) {
      int remaining = openConnections.decrementAndGet();
      assert remaining >= 0;
      Host.statesLogger.debug("[{}] {} closed, remaining = {}", host, connection, remaining);
    }

    @Override
    boolean signalConnectionFailure(Connection connection, boolean decrement) {
      int remaining;
      if (decrement) {
        if (host.state != Host.State.DOWN) updateReconnectionTime();

        remaining = openConnections.decrementAndGet();
        assert remaining >= 0;
        Host.statesLogger.debug("[{}] {} failed, remaining = {}", host, connection, remaining);
      } else {
        remaining = openConnections.get();
      }
      return remaining == 0;
    }

    private synchronized void updateReconnectionTime() {
      long now = System.nanoTime();
      if (nextReconnectionTime > now)
        // Someone else updated the time before us
        return;

      if (reconnectionSchedule == null) reconnectionSchedule = reconnectionPolicy.newSchedule();

      long nextDelayMs = reconnectionSchedule.nextDelayMs();
      Host.statesLogger.debug(
          "[{}] preventing new connections for the next {} ms", host, nextDelayMs);
      nextReconnectionTime = now + NANOSECONDS.convert(nextDelayMs, MILLISECONDS);
    }

    private synchronized void resetReconnectionTime() {
      reconnectionSchedule = null;
      nextReconnectionTime = Long.MIN_VALUE;
    }

    @Override
    boolean canReconnectNow() {
      boolean canReconnectNow =
          nextReconnectionTime == Long.MIN_VALUE || System.nanoTime() >= nextReconnectionTime;
      Host.statesLogger.trace("canReconnectNow={}", canReconnectNow);
      return canReconnectNow;
    }

    @Override
    boolean hasActiveConnections() {
      return openConnections.get() > 0;
    }

    static class Factory implements ConvictionPolicy.Factory {

      @Override
      public ConvictionPolicy create(Host host, ReconnectionPolicy reconnectionPolicy) {
        return new DefaultConvictionPolicy(host, reconnectionPolicy);
      }
    }
  }
}
