/*
 *  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.tools.ant;

import java.io.IOException;
import java.util.Enumeration;

import org.apache.tools.ant.dispatch.DispatchUtils;

/**
 * Base class for all tasks.
 *
 * Use Project.createTask to create a new task instance rather than
 * using this class directly for construction.
 *
 * @see Project#createTask
 */
public abstract class Task extends ProjectComponent {
    // CheckStyle:VisibilityModifier OFF - bc
    /**
     * Target this task belongs to, if any.
     * @deprecated since 1.6.x.
     *             You should not be accessing this variable directly.
     *             Please use the {@link #getOwningTarget()} method.
     */
    protected Target target;

    /**
     * Name of this task to be used for logging purposes.
     * This defaults to the same as the type, but may be
     * overridden by the user. For instance, the name "java"
     * isn't terribly descriptive for a task used within
     * another task - the outer task code can probably
     * provide a better one.
     * @deprecated since 1.6.x.
     *             You should not be accessing this variable directly.
     *             Please use the {@link #getTaskName()} method.
     */
    protected String taskName;

    /**
     * Type of this task.
     *
     * @deprecated since 1.6.x.
     *             You should not be accessing this variable directly.
     *             Please use the {@link #getTaskType()} method.
     */
    protected String taskType;

    /**
     * Wrapper for this object, used to configure it at runtime.
     *
     * @deprecated since 1.6.x.
     *             You should not be accessing this variable directly.
     *             Please use the {@link #getWrapper()} method.
     */
    protected RuntimeConfigurable wrapper;

    // CheckStyle:VisibilityModifier ON

    /**
     * Whether or not this task is invalid. A task becomes invalid
     * if a conflicting class is specified as the implementation for
     * its type.
     */
    private boolean invalid;

    /**
     * Sets the target container of this task.
     *
     * @param target Target in whose scope this task belongs.
     *               May be <code>null</code>, indicating a top-level task.
     */
    public void setOwningTarget(Target target) {
        this.target = target;
    }

    /**
     * Returns the container target of this task.
     *
     * @return The target containing this task, or <code>null</code> if
     *         this task is a top-level task.
     */
    public Target getOwningTarget() {
        return target;
    }

    /**
     * Sets the name to use in logging messages.
     *
     * @param name The name to use in logging messages.
     *             Should not be <code>null</code>.
     */
    public void setTaskName(String name) {
        this.taskName = name;
    }

    /**
     * Returns the name to use in logging messages.
     *
     * @return the name to use in logging messages.
     */
    public String getTaskName() {
        return taskName;
    }

    /**
     * Sets the name with which the task has been invoked.
     *
     * @param type The name the task has been invoked as.
     *             Should not be <code>null</code>.
     */
    public void setTaskType(String type) {
        this.taskType = type;
    }

    /**
     * Called by the project to let the task initialize properly.
     * The default implementation is a no-op.
     *
     * @exception BuildException if something goes wrong with the build
     */
    public void init() throws BuildException {
    }

    /**
     * Called by the project to let the task do its work. This method may be
     * called more than once, if the task is invoked more than once.
     * For example,
     * if target1 and target2 both depend on target3, then running
     * "ant target1 target2" will run all tasks in target3 twice.
     *
     * @exception BuildException if something goes wrong with the build.
     */
    public void execute() throws BuildException {
    }

    /**
     * Returns the wrapper used for runtime configuration.
     *
     * @return the wrapper used for runtime configuration. This
     *         method will generate a new wrapper (and cache it)
     *         if one isn't set already.
     */
    public RuntimeConfigurable getRuntimeConfigurableWrapper() {
        if (wrapper == null) {
            wrapper = new RuntimeConfigurable(this, getTaskName());
        }
        return wrapper;
    }

    /**
     * Sets the wrapper to be used for runtime configuration.
     *
     * This method should be used only by the ProjectHelper and Ant internals.
     * It is public to allow helper plugins to operate on tasks, normal tasks
     * should never use it.
     *
     * @param wrapper The wrapper to be used for runtime configuration.
     *                May be <code>null</code>, in which case the next call
     *                to getRuntimeConfigurableWrapper will generate a new
     *                wrapper.
     */
    public void setRuntimeConfigurableWrapper(RuntimeConfigurable wrapper) {
        this.wrapper = wrapper;
    }

    // TODO: (Jon Skeet) The comment "if it hasn't been done already" may
    // not be strictly true. wrapper.maybeConfigure() won't configure the same
    // attributes/text more than once, but it may well add the children again,
    // unless I've missed something.
    /**
     * Configures this task - if it hasn't been done already.
     * If the task has been invalidated, it is replaced with an
     * UnknownElement task which uses the new definition in the project.
     *
     * @exception BuildException if the task cannot be configured.
     */
    public void maybeConfigure() throws BuildException {
        if (invalid) {
            getReplacement();
        } else if (wrapper != null) {
            wrapper.maybeConfigure(getProject());
        }
    }

    /**
     * Force the task to be reconfigured from its RuntimeConfigurable.
     */
    public void reconfigure() {
        if (wrapper != null) {
            wrapper.reconfigure(getProject());
        }
    }

    /**
     * Handles output by logging it with the INFO priority.
     *
     * @param output The output to log. Should not be <code>null</code>.
     */
    protected void handleOutput(String output) {
        log(output, Project.MSG_INFO);
    }

    /**
     * Handles output by logging it with the INFO priority.
     *
     * @param output The output to log. Should not be <code>null</code>.
     *
     * @since Ant 1.5.2
     */
    protected void handleFlush(String output) {
        handleOutput(output);
    }

    /**
     * Handle an input request by this task.
     *
     * @param buffer the buffer into which data is to be read.
     * @param offset the offset into the buffer at which data is stored.
     * @param length the amount of data to read.
     *
     * @return the number of bytes read.
     *
     * @exception IOException if the data cannot be read.
     * @since Ant 1.6
     */
    protected int handleInput(byte[] buffer, int offset, int length)
        throws IOException {
        return getProject().defaultInput(buffer, offset, length);
    }

    /**
     * Handles an error output by logging it with the WARN priority.
     *
     * @param output The error output to log. Should not be <code>null</code>.
     */
    protected void handleErrorOutput(String output) {
        log(output, Project.MSG_WARN);
    }

    /**
     * Handles an error line by logging it with the WARN priority.
     *
     * @param output The error output to log. Should not be <code>null</code>.
     *
     * @since Ant 1.5.2
     */
    protected void handleErrorFlush(String output) {
        handleErrorOutput(output);
    }

    /**
     * Logs a message with the default (INFO) priority.
     *
     * @param msg The message to be logged. Should not be <code>null</code>.
     */
    public void log(String msg) {
        log(msg, Project.MSG_INFO);
    }

    /**
     * Logs a message with the given priority. This delegates
     * the actual logging to the project.
     *
     * @param msg The message to be logged. Should not be <code>null</code>.
     * @param msgLevel The message priority at which this message is to
     *                 be logged.
     */
    public void log(String msg, int msgLevel) {
        if (getProject() == null) {
            super.log(msg, msgLevel);
        } else {
            getProject().log(this, msg, msgLevel);
        }
    }

    /**
     * Logs a message with the given priority. This delegates
     * the actual logging to the project.
     *
     * @param t The exception to be logged. Should not be <code>null</code>.
     * @param msgLevel The message priority at which this message is to
     *                 be logged.
     * @since 1.7
     */
    public void log(Throwable t, int msgLevel) {
        if (t != null) {
            log(t.getMessage(), t, msgLevel);
        }
    }

    /**
     * Logs a message with the given priority. This delegates
     * the actual logging to the project.
     *
     * @param msg The message to be logged. Should not be <code>null</code>.
     * @param t The exception to be logged. May be <code>null</code>.
     * @param msgLevel The message priority at which this message is to
     *                 be logged.
     * @since 1.7
     */
    public void log(String msg, Throwable t, int msgLevel) {
        if (getProject() == null) {
            super.log(msg, msgLevel);
        } else {
            getProject().log(this, msg, t, msgLevel);
        }
    }

    /**
     * Performs this task if it's still valid, or gets a replacement
     * version and performs that otherwise.
     *
     * Performing a task consists of firing a task started event,
     * configuring the task, executing it, and then firing task finished
     * event. If a runtime exception is thrown, the task finished event
     * is still fired, but with the exception as the cause.
     */
    public final void perform() {
        if (invalid) {
            UnknownElement ue = getReplacement();
            Task task = ue.getTask();
            task.perform();
        } else {
            getProject().fireTaskStarted(this);
            Throwable reason = null;
            try {
                maybeConfigure();
                DispatchUtils.execute(this);
            } catch (BuildException ex) {
                if (ex.getLocation() == Location.UNKNOWN_LOCATION) {
                    ex.setLocation(getLocation());
                }
                reason = ex;
                throw ex;
            } catch (Exception ex) {
                reason = ex;
                BuildException be = new BuildException(ex);
                be.setLocation(getLocation());
                throw be;
            } catch (Error ex) {
                reason = ex;
                throw ex;
            } finally {
                getProject().fireTaskFinished(this, reason);
            }
        }
    }

    /**
     * Marks this task as invalid. Any further use of this task
     * will go through a replacement with the updated definition.
     */
    final void markInvalid() {
        invalid = true;
    }

    /**
     * Has this task been marked invalid?
     *
     * @return true if this task is no longer valid. A new task should be
     * configured in this case.
     *
     * @since Ant 1.5
     */
    protected final boolean isInvalid() {
        return invalid;
    }

    /**
     * Replacement element used if this task is invalidated.
     */
    private UnknownElement replacement;

    /**
     * Creates an UnknownElement that can be used to replace this task.
     * Once this has been created once, it is cached and returned by
     * future calls.
     *
     * @return the UnknownElement instance for the new definition of this task.
     */
    private UnknownElement getReplacement() {
        if (replacement == null) {
            replacement = new UnknownElement(taskType);
            replacement.setProject(getProject());
            replacement.setTaskType(taskType);
            replacement.setTaskName(taskName);
            replacement.setLocation(getLocation());
            replacement.setOwningTarget(target);
            replacement.setRuntimeConfigurableWrapper(wrapper);
            wrapper.setProxy(replacement);
            replaceChildren(wrapper, replacement);
            target.replaceChild(this, replacement);
            replacement.maybeConfigure();
        }
        return replacement;
    }

    /**
     * Recursively adds an UnknownElement instance for each child
     * element of replacement.
     *
     * @since Ant 1.5.1
     */
    private void replaceChildren(RuntimeConfigurable wrapper,
                                 UnknownElement parentElement) {
        Enumeration<RuntimeConfigurable> e = wrapper.getChildren();
        while (e.hasMoreElements()) {
            RuntimeConfigurable childWrapper = e.nextElement();
            UnknownElement childElement =
                new UnknownElement(childWrapper.getElementTag());
            parentElement.addChild(childElement);
            childElement.setProject(getProject());
            childElement.setRuntimeConfigurableWrapper(childWrapper);
            childWrapper.setProxy(childElement);
            replaceChildren(childWrapper, childElement);
        }
    }

    /**
     * Return the type of task.
     *
     * @return the type of task.
     */
    public String getTaskType() {
        return taskType;
    }

    /**
     * Return the runtime configurable structure for this task.
     *
     * @return the runtime structure for this task.
     */
    protected RuntimeConfigurable getWrapper() {
        return wrapper;
    }

    /**
     * Bind a task to another; use this when configuring a newly created
     * task to do work on behalf of another.
     * Project, OwningTarget, TaskName, Location and Description are all copied
     *
     * Important: this method does not call {@link Task#init()}.
     * If you are creating a task to delegate work to, call {@link Task#init()}
     * to initialize it.
     *
     * @param owner owning target
     * @since Ant1.7
     */
    public final void bindToOwner(Task owner) {
        setProject(owner.getProject());
        setOwningTarget(owner.getOwningTarget());
        setTaskName(owner.getTaskName());
        setDescription(owner.getDescription());
        setLocation(owner.getLocation());
        setTaskType(owner.getTaskType());
    }
}
