/*
 * 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.commons.pool2.impl;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;

import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.Waiter;
import org.apache.commons.pool2.WaiterFactory;

public class DisconnectingWaiterFactory<K> extends WaiterFactory<K> {
    private static final Duration DEFAULT_TIME_BETWEEN_CONNECTION_CHECKS = Duration.ofMillis(100);

    private static final Duration DEFAULT_MAX_WAIT = Duration.ofSeconds(10);

    /**
     * Default function to perform for activate, passivate, destroy in disconnected
     * mode - no-op
     */
    protected static final Consumer<PooledObject<Waiter>> DEFAULT_DISCONNECTED_LIFECYCLE_ACTION = w -> {
    };

    /**
     * Default supplier determining makeObject action when invoked in disconnected
     * mode. Default behavior is to block until reconnected for up to maxWait
     * duration. If maxWait is exceeded, throw ISE; if reconnect happens in time,
     * return a new DefaultPooledObject<Waiter>.
     */
    protected static final Supplier<PooledObject<Waiter>> DEFAULT_DISCONNECTED_CREATE_ACTION = () -> {
        waitForConnection(null, DEFAULT_TIME_BETWEEN_CONNECTION_CHECKS, DEFAULT_MAX_WAIT);
        return new DefaultPooledObject<>(new Waiter(true, true, 0));
    };

    /**
     * Default predicate determining what validate does in disconnected state -
     * default is to always return false
     */
    protected static final Predicate<PooledObject<Waiter>> DEFAULT_DISCONNECTED_VALIDATION_ACTION = w -> false;

    /**
     * Blocks until connected or maxWait is exceeded.
     * 
     * @throws TimeoutException if maxWait is exceeded.
     */
    private static void waitForConnection(final AtomicBoolean connected,
            final Duration timeBetweenConnectionChecks, final Duration maxWait) {
        final Instant start = Instant.now();
        while (!connected.get()) {
            try {
                Thread.sleep(timeBetweenConnectionChecks.toMillis());
            } catch (final InterruptedException e) {
                e.printStackTrace();
            }
            if (Duration.between(start, Instant.now()).compareTo(maxWait) > 0) {
                throw new IllegalStateException(new TimeoutException("Timed out waiting for connection"));
            }
        }
    }

    /**
     * 
     * A WaiterFactory that simulates a resource required by factory methods going
     * down (and coming back).
     * <p>
     * When connected, this factory behaves like a normal WaiterFactory.
     * When disconnected, factory methods are determined by functional parameters.
     * </p>
     */
    private final AtomicBoolean connected = new AtomicBoolean(true);

    /** Time between reconnection checks */
    final Duration timeBetweenConnectionChecks;

    /**
     * Maximum amount of time a factory method will wait for reconnect before
     * throwing TimeOutException
     */
    final Duration maxWait;

    /** Function to perform when makeObject is executed in disconnected mode */
    final Supplier<PooledObject<Waiter>> disconnectedCreateAction;

    /**
     * Function to perform for activate, passsivate and destroy when invoked in
     * disconnected mode
     */
    final Consumer<PooledObject<Waiter>> disconnectedLifcycleAction;

    /** Function to perform for validate when invoked in disconnected mode */
    final Predicate<PooledObject<Waiter>> disconnectedValidationAction;

    public DisconnectingWaiterFactory() {
        this(DEFAULT_DISCONNECTED_CREATE_ACTION, DEFAULT_DISCONNECTED_LIFECYCLE_ACTION,
                DEFAULT_DISCONNECTED_VALIDATION_ACTION);
    }

    public DisconnectingWaiterFactory(final long activateLatency, final long destroyLatency,
            final long makeLatency, final long passivateLatency, final long validateLatency,
            final long waiterLatency) {
        this(activateLatency, destroyLatency, makeLatency, passivateLatency,
                validateLatency, waiterLatency, Long.MAX_VALUE, Long.MAX_VALUE, 0);
    }

    public DisconnectingWaiterFactory(final long activateLatency, final long destroyLatency,
            final long makeLatency, final long passivateLatency, final long validateLatency,
            final long waiterLatency, final long maxActive) {
        this(activateLatency, destroyLatency, makeLatency, passivateLatency,
                validateLatency, waiterLatency, maxActive,
                Long.MAX_VALUE, 0);
    }

    public DisconnectingWaiterFactory(final long activateLatency, final long destroyLatency,
            final long makeLatency, final long passivateLatency, final long validateLatency,
            final long waiterLatency, final long maxActive, final long maxActivePerKey,
            final double passivateInvalidationProbability) {
        super(activateLatency, destroyLatency, makeLatency, passivateLatency,
                validateLatency, waiterLatency, maxActive, maxActivePerKey,
                passivateInvalidationProbability);
        this.timeBetweenConnectionChecks = DEFAULT_TIME_BETWEEN_CONNECTION_CHECKS;
        this.maxWait = DEFAULT_MAX_WAIT;
        this.disconnectedCreateAction = DEFAULT_DISCONNECTED_CREATE_ACTION;
        this.disconnectedLifcycleAction = DEFAULT_DISCONNECTED_LIFECYCLE_ACTION;
        this.disconnectedValidationAction = DEFAULT_DISCONNECTED_VALIDATION_ACTION;
    }

    public DisconnectingWaiterFactory(final Supplier<PooledObject<Waiter>> disconnectedCreateAction,
            final Consumer<PooledObject<Waiter>> disconnectedLifcycleAction,
            final Predicate<PooledObject<Waiter>> disconnectedValidationAction) {
        super(0, 0, 0,
                0, 0, 0,
                Integer.MAX_VALUE,
                Integer.MAX_VALUE,
                0);
        this.timeBetweenConnectionChecks = DEFAULT_TIME_BETWEEN_CONNECTION_CHECKS;
        this.maxWait = DEFAULT_MAX_WAIT;
        this.disconnectedCreateAction = disconnectedCreateAction;
        this.disconnectedLifcycleAction = disconnectedLifcycleAction;
        this.disconnectedValidationAction = disconnectedValidationAction;
    }

    private void activate(final PooledObject<Waiter> obj) {
        if (connected.get()) {
            super.activateObject(obj);
        } else {
            disconnectedLifcycleAction.accept(obj);
        }
    }

    @Override
    public void activateObject(final K key, final PooledObject<Waiter> obj) {
        activate(obj);
    }

    @Override
    public void activateObject(final PooledObject<Waiter> obj) {
        activate(obj);
    }

    /**
     * Reconnect the factory.
     */
    public void connect() {
        connected.set(true);
    }
    /*
     * TODO: add builder to clean up constructors and make maxWait,
     * timeBetweenConnectionChecks configurable.
     */

    /**
     * Disconnect the factory.
     */
    public void disconnect() {
        connected.set(false);
    }

    private PooledObject<Waiter> make() {
        if (connected.get()) {
            return super.makeObject();
        }
        return disconnectedCreateAction.get();
    }

    @Override
    public PooledObject<Waiter> makeObject() {
        return make();
    }

    @Override
    public PooledObject<Waiter> makeObject(final K key) {
        return make();
    }

    private void passivate(final PooledObject<Waiter> obj) {
        if (connected.get()) {
            super.passivateObject(obj);
        } else {
            disconnectedLifcycleAction.accept(obj);
        }
    }

    @Override
    public void passivateObject(final K key, final PooledObject<Waiter> obj) {
        passivate(obj);
    }

    @Override
    public void passivateObject(final PooledObject<Waiter> obj) {
        passivate(obj);
    }

    private boolean validate(final PooledObject<Waiter> obj) {
        if (connected.get()) {
            return super.validateObject(obj);
        }
        return disconnectedValidationAction.test(obj);
    }

    @Override
    public boolean validateObject(final K key, final PooledObject<Waiter> obj) {
        return validate(obj);
    }

    @Override
    public boolean validateObject(final PooledObject<Waiter> obj) {
        return validate(obj);
    }

}
