/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2012, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.patching.runner;

import static org.jboss.as.patching.IoUtils.NO_CONTENT;

import java.io.IOException;
import java.util.Arrays;

import org.jboss.as.patching.PatchingException;
import org.jboss.as.patching.logging.PatchLogger;
import org.jboss.as.patching.metadata.ContentItem;
import org.jboss.as.patching.metadata.ContentModification;
import org.jboss.as.patching.metadata.ContentType;
import org.jboss.as.patching.metadata.ModificationCondition;

/**
 * Basic patching task implementation.
 *
 * @author Emanuel Muckenhuber
 */
abstract class AbstractPatchingTask<T extends ContentItem> implements PatchingTask {

    protected final T contentItem;
    protected final PatchingTaskDescription description;

    private boolean ignoreApply;   // completely ignore the apply step
    private boolean skipExecution; // Skip the execution step
    protected byte[] backupHash = NO_CONTENT; // The backup hash

    AbstractPatchingTask(PatchingTaskDescription description, Class<T> expected) {
        this.description = description;
        this.contentItem = description.getContentItem(expected);
    }

    @Override
    public T getContentItem() {
        return contentItem;
    }

    @Override
    public boolean isRelevant(PatchingTaskContext context) throws PatchingException {
        final ModificationCondition condition = description.getModification().getCondition();
        if(condition == null) {
            return true;
        }
        return condition.isSatisfied(context);
    }

    /**
     * Backup the content.
     *
     * @param context the patching context
     * @return the hash of the content
     * @throws IOException
     */
    abstract byte[] backup(PatchingTaskContext context) throws IOException;

    /**
     * Apply the modification.
     *
     * @param context the patching context
     * @param loader the patch content loader
     * @return the actual copied content hash
     * @throws IOException
     */
    abstract byte[] apply(final PatchingTaskContext context, final PatchContentLoader loader) throws IOException;

    /**
     * Create the rollback entry.
     *
     * @param original the original content modification
     * @param targetHash the new target hash code (current content)
     * @param itemHash the new content item hash (backup content)
     * @return the rollback modification information
     */
    abstract ContentModification createRollbackEntry(ContentModification original, byte[] targetHash, byte[] itemHash);

    /**
     * Fail if the copied content is different from the one specified in the metadata. This should be true in most of the
     * cases. Only removing a module does not really match this, since we are creating a removed-module rather than
     * removing the contents.
     *
     * @param context the task context
     * @return
     */
    protected boolean failOnContentMismatch(PatchingTaskContext context) {
        return context.getCurrentMode() != PatchingTaskContext.Mode.UNDO;
    }

    /**
     * Get the original modification. Tasks like module remove can override this and fix the hashes based on the created content.
     *
     * @param targetHash the new target hash code (current content)
     * @param itemHash the new content item hash (backup content)
     * @return the original modification
     */
    protected ContentModification getOriginalModification(byte[] targetHash, byte[] itemHash) {
        return description.getModification();
    }

    /**
     * Completely skip the apply step.
     */
    protected void setIgnoreApply() {
        ignoreApply = true;
    }

    @Override
    public boolean prepare(final PatchingTaskContext context) throws IOException {
        // Backup
        backupHash = backup(context);
        // If the content is already present just resolve any conflict automatically
        final byte[] contentHash = contentItem.getContentHash();
        if(Arrays.equals(backupHash, contentHash)) {
            // Skip execute for misc items only
            skipExecution = contentItem.getContentType() == ContentType.MISC && backupHash != NO_CONTENT;
            return true;
        }
        // See if the content matches our expected target
        final byte[] expected = description.getModification().getTargetHash();
        if(Arrays.equals(backupHash, expected)) {
            // Don't resolve conflicts from the history
            return ! description.hasConflicts();
        }
        return false;
    }

    @Override
    public void execute(final PatchingTaskContext context) throws IOException {
        if (ignoreApply) {
            return;
        }
        final PatchContentLoader contentLoader = description.getLoader();
        final boolean skip = skipExecution || context.isExcluded(contentItem);
        final byte[] contentHash;
        if(skip) {
            contentHash = backupHash; // Reuse the backup hash
        } else {
            contentHash = apply(context, contentLoader); // Copy the content
        }
        // Add the rollback action
        final ContentModification original = getOriginalModification(contentHash, backupHash);
        final ContentModification rollbackAction = createRollbackEntry(original, contentHash, backupHash);
        context.recordChange(original, rollbackAction);
        // Fail after adding the undo action
        if (! Arrays.equals(contentHash, contentItem.getContentHash()) && failOnContentMismatch(context) && !context.isIgnored(contentItem)) {
            throw PatchLogger.ROOT_LOGGER.wrongCopiedContent(contentItem);
        }
    }

}
