/*
 * 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
 *
 *      https://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.lang3.concurrent;

import java.util.Objects;

import org.apache.commons.lang3.builder.AbstractSupplier;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.function.FailableConsumer;
import org.apache.commons.lang3.function.FailableSupplier;

/**
 * Abstracts and defines operations for {@link ConcurrentInitializer} implementations.
 *
 * @param <T> the type of the object managed by this initializer class.
 * @param <E> The exception type thrown by {@link #initialize()}.
 * @since 3.14.0
 */
public abstract class AbstractConcurrentInitializer<T, E extends Exception> implements ConcurrentInitializer<T> {

    /**
     * Builds a new instance for subclasses.
     *
     * @param <I> The type of results supplied by this builder.
     * @param <T> The type of the object managed by the initializer class.
     * @param <B> The type of builder.
     * @param <E> The exception type thrown by {@link #initialize()}.
     */
    public abstract static class AbstractBuilder<I extends AbstractConcurrentInitializer<T, E>, T, B extends AbstractBuilder<I, T, B, E>, E extends Exception>
            extends AbstractSupplier<I, B, E> {

        /**
         * Closer consumer called by {@link #close()}.
         */
        private FailableConsumer<T, ? extends Exception> closer = FailableConsumer.nop();

        /**
         * Initializer supplier called by {@link #initialize()}.
         */
        private FailableSupplier<T, ? extends Exception> initializer = FailableSupplier.nul();

        /**
         * Constructs a new instance.
         */
        public AbstractBuilder() {
            // empty
        }

        /**
         * Gets the closer consumer called by {@link #close()}.
         *
         * @return the closer consumer called by {@link #close()}.
         */
        public FailableConsumer<T, ? extends Exception> getCloser() {
            return closer;
        }

        /**
         * Gets the initializer supplier called by {@link #initialize()}.
         *
         * @return the initializer supplier called by {@link #initialize()}.
         */
        public FailableSupplier<T, ? extends Exception> getInitializer() {
            return initializer;
        }

        /**
         * Sets the closer consumer called by {@link #close()}.
         *
         * @param closer the consumer called by {@link #close()}.
         * @return {@code this} instance.
         */
        public B setCloser(final FailableConsumer<T, ? extends Exception> closer) {
            this.closer = closer != null ? closer : FailableConsumer.nop();
            return asThis();
        }

        /**
         * Sets the initializer supplier called by {@link #initialize()}.
         *
         * @param initializer the supplier called by {@link #initialize()}.
         * @return {@code this} instance.
         */
        public B setInitializer(final FailableSupplier<T, ? extends Exception> initializer) {
            this.initializer = initializer != null ? initializer : FailableSupplier.nul();
            return asThis();
        }

    }

    /**
     * Closer consumer called by {@link #close()}.
     */
    private final FailableConsumer<? super T, ? extends Exception> closer;

    /**
     * Initializer supplier called by {@link #initialize()}.
     */
    private final FailableSupplier<? extends T, ? extends Exception> initializer;

    /**
     * Constructs a new instance.
     */
    public AbstractConcurrentInitializer() {
        this(FailableSupplier.nul(), FailableConsumer.nop());
    }

    /**
     * Constructs a new instance.
     *
     * @param initializer the initializer supplier called by {@link #initialize()}.
     * @param closer the closer consumer called by {@link #close()}.
     */
    AbstractConcurrentInitializer(final FailableSupplier<? extends T, ? extends Exception> initializer, final FailableConsumer<? super T, ? extends Exception> closer) {
        this.closer = Objects.requireNonNull(closer, "closer");
        this.initializer = Objects.requireNonNull(initializer, "initializer");
    }

    /**
     * Calls the closer with the manager object.
     *
     * @throws ConcurrentException Thrown by the closer.
     * @since 3.14.0
     */
    public void close() throws ConcurrentException {
        if (isInitialized()) {
            try {
                closer.accept(get());
            } catch (final Exception e) {
                // This intentionally does not duplicate the logic in initialize
                // or care about the generic type E.
                //
                // initialize may run inside a Future and it does not make sense
                // to wrap an exception stored inside a Future. However close()
                // always runs on the current thread so it always wraps in a
                // ConcurrentException
                throw new ConcurrentException(ExceptionUtils.throwUnchecked(e));
            }
        }
    }

    /**
     * Gets an Exception with a type of E as defined by a concrete subclass of this class.
     *
     * @param e The actual exception that was thrown.
     * @return a new exception with the actual type of E, that wraps e.
     */
    protected abstract E getTypedException(Exception e);

    /**
     * Creates and initializes the object managed by this {@code
     * ConcurrentInitializer}. This method is called by {@link #get()} when the object is accessed for the first time. An implementation can focus on the
     * creation of the object. No synchronization is needed, as this is already handled by {@code get()}.
     * <p>
     * Subclasses and clients that do not provide an initializer are expected to implement this method.
     * </p>
     *
     * @return the managed data object.
     * @throws E if an error occurs during object creation.
     */
    @SuppressWarnings("unchecked")
    protected T initialize() throws E {
        try {
            return initializer.get();
        } catch (final Exception e) {
            // Do this first so we don't pass a RuntimeException or Error into an exception constructor
            ExceptionUtils.throwUnchecked(e);
            // Depending on the subclass of AbstractConcurrentInitializer E can be Exception or ConcurrentException
            // if E is Exception the if statement below will always be true, and the new Exception object created
            // in getTypedException will never be thrown. If E is ConcurrentException and the if statement is false
            // we throw the ConcurrentException returned from getTypedException, which wraps the original exception.
            final E typedException = getTypedException(e);
            if (typedException.getClass().isAssignableFrom(e.getClass())) {
                throw (E) e;
            }
            throw typedException;
        }
    }

    /**
     * Tests whether initialization has been completed. If initialization threw an exception this will return false, but it will return true if a subsequent
     * call to initialize completes successfully. If the implementation of ConcurrentInitializer can initialize multiple objects, this will only return true if
     * all objects have been initialized.
     *
     * @return true if all initialization is complete, otherwise false.
     */
    protected abstract boolean isInitialized();

}
