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

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.apache.commons.chain2.Chain;
import org.apache.commons.chain2.ChainException;
import org.apache.commons.chain2.Command;
import org.apache.commons.chain2.Context;
import org.apache.commons.chain2.Filter;
import org.apache.commons.chain2.Processing;
import org.apache.commons.logging.LogFactory;

/**
 * <p>Convenience base class for {@link Chain} implementations.</p>
 *
 * @version $Id$
 *
 * @param <K> the type of keys maintained by the context associated with this chain
 * @param <V> the type of mapped values
 * @param <C> Type of the context associated with this chain
 */
public class ChainBase<K, V, C extends Map<K, V>> implements Chain<K, V, C> {

    // ----------------------------------------------------- Instance Variables

    /**
     * <p>The list of {@link Command}s configured for this {@link Chain}, in
     * the order in which they may delegate processing to the remainder of
     * the {@link Chain}.</p>
     */
    private final List<Command<K, V, C>> commands = new ArrayList<>();

    /**
     * <p>Flag indicating whether the configuration of our commands list
     * has been frozen by a call to the <code>execute()</code> method.</p>
     */
    private boolean frozen = false;

    // ----------------------------------------------------------- Constructors

    /**
     * <p>Construct a {@link Chain} with no configured {@link Command}s.</p>
     */
    public ChainBase() {
        super();
    }

    /**
     * <p>Construct a {@link Chain} configured with the specified
     * {@link Command}.</p>
     *
     * @param command The {@link Command} to be configured
     *
     * @throws IllegalArgumentException if <code>command</code>
     *  is <code>null</code>
     */
    public ChainBase(final Command<K, V, C> command) {
        addCommand(command);
    }

    /**
     * <p>Construct a {@link Chain} configured with the specified
     * {@link Command Commands}.</p>
     * @param cmds The {@link Command Commands} to be configured
     *
     * @throws IllegalArgumentException if <code>commands</code>,
     *  or one of the individual {@link Command} elements,
     *  is <code>null</code>
     */
    @SafeVarargs
    public ChainBase(final Command<K, V, C>... cmds) {
        if (cmds == null) {
            throw new IllegalArgumentException();
        }

        for (final Command<K, V, C> command : cmds) {
            addCommand(command);
        }
    }

    /**
     * <p>Construct a {@link Chain} configured with the specified
     * {@link Command}s.</p>
     *
     * @param cmds The {@link Command}s to be configured
     *
     * @throws IllegalArgumentException if <code>commands</code>,
     *  or one of the individual {@link Command} elements,
     *  is <code>null</code>
     */
    public ChainBase(final Collection<Command<K, V, C>> cmds) {
        if (cmds == null) {
            throw new IllegalArgumentException();
        }
        for (final Command<K, V, C> command : cmds) {
            addCommand(command);
        }
    }

    // ---------------------------------------------------------- Chain Methods

    /**
     * See the {@link Chain} JavaDoc.
     *
     * @param <T> the {@link Command} type to be added in the {@link Chain}
     * @param command The {@link Command} to be added
     * @throws IllegalArgumentException if <code>command</code>
     *  is <code>null</code>
     * @throws IllegalStateException if no further configuration is allowed
     */
    @Override
    public <T extends Command<K, V, C>> void addCommand(final T command) {
        if (command == null) {
            throw new IllegalArgumentException();
        }
        if (this.frozen) {
            throw new IllegalStateException();
        }
        this.commands.add(command);
    }

    /**
     * See the {@link Chain} JavaDoc.
     *
     * @param context The {@link Context} to be processed by this
     *  {@link Chain}
     *
     *  in this {@link Chain} but not handled by a <code>postprocess()</code>
     *  method of a {@link Filter}
     * @throws IllegalArgumentException if <code>context</code>
     *  is <code>null</code>
     * @return {@link Processing#FINISHED} if the processing of this context
     *  has been completed. Returns {@link Processing#CONTINUE} if the processing
     *  of this context should be delegated to a subsequent command in an
     *  enclosing chain.
     */
    @Override
    public Processing execute(final C context) {
        // Verify our parameters
        if (context == null) {
            throw new IllegalArgumentException("Can't execute a null context");
        }

        // Freeze the configuration of the command list
        this.frozen = true;

        // Execute the commands in this list until one returns something else
        // than Processing.CONTINUE or throws an exception
        Processing saveResult = Processing.CONTINUE;
        Exception saveException = null;
        int i;
        int n = this.commands.size();
        Command<K, V, C> lastCommand = null;
        for (i = 0; i < n; i++) {
            try {
                lastCommand = this.commands.get(i);
                saveResult = lastCommand.execute(context);
                if (saveResult == null) {
                    String format = String.format(
                            "The command '%s' returned an invalid processing value: '%s'",
                            lastCommand.getClass().getName(), null);
                    throw new ChainException(format);
                } else if (saveResult != Processing.CONTINUE) {
                    break;
                }
            } catch (final ChainException e) {
                saveException = e;
                break;
            }
        }

        // Call postprocess methods on Filters in reverse order
        // Fell off the end of the chain
        if (i >= n) {
            i--;
        }

        boolean handled = false;
        boolean result;
        for (int j = i; j >= 0; j--) {
            if (this.commands.get(j) instanceof Filter) {
                try {
                    result = ((Filter<K, V, C>) this.commands.get(j)).postprocess(
                            context, saveException);
                    if (result) {
                        handled = true;
                    }
                } catch (final Throwable e) {
                    // Silently ignore
                    LogFactory.getLog(ChainBase.class).warn(e.getMessage());
                }
            }
        }

        // Return the exception or result state from the last execute()
        if (saveException != null && !handled) {
            // Wrap and rethrow exception
            throw wrapUnhandledExceptions(saveException, context, lastCommand);
        }

        return saveResult;
    }

    /**
     * Wraps an unhandled exception in a {@link ChainException} class.
     * @param unhandled Unhandled exception as passed from failing command
     * @param context Context that was passed to failing command
     * @param failedCommand The command that failed
     * @return Original unhandled exception wrapped in a ChainException
     */
    protected ChainException wrapUnhandledExceptions(
            final Throwable unhandled, final C context, final Command<K, V, C> failedCommand) {
        /* There should not be a reason to rewrap an exception that is already
         * wrapped in a ChainException because the first wrapping preserves
         * the {@link Context} necessary for debugging. Adding more wrapped
         * exceptions would just make the debugging process more difficult. */
        if (unhandled instanceof ChainException) {
            return (ChainException) unhandled;
        }

        String msg = (failedCommand == null) ? "An error occurred when executing the chain"
                : String.format("An error occurred when executing the command %s in the chain",
                               failedCommand.getClass().getName());

        return new ChainException(msg, unhandled, context, failedCommand);
    }

    /**
     * Returns true, if the configuration of our commands list
     * has been frozen by a call to the <code>execute()</code> method,
     * false otherwise.
     *
     * @return true, if the configuration of our commands list
     * has been frozen by a call to the <code>execute()</code> method,
     * false otherwise.
     * @since 2.0
     */
    public boolean isFrozen() {
        return this.frozen;
    }

    // -------------------------------------------------------- Package Methods
    /**
     * <p>Return an array of the configured {@link Command}s for this
     * {@link Chain}.  This method is package private, and is used only
     * for the unit tests.</p>
     * @return List
     */
    List<Command<K, V, C>> getCommands() {
        return this.commands;
    }

}
