/*
 * This file is part of the OSMembrane project.
 * More informations under www.osmembrane.de
 * 
 * The project is licensed under the GNU GENERAL PUBLIC LICENSE 3.0.
 * for more details about the license see http://www.osmembrane.de/license/
 * 
 * Source: $HeadURL$ ($Revision$)
 * Last changed: $Date$
 */

package de.osmembrane.model.pipeline;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import java.util.Stack;

import de.osmembrane.model.ModelProxy;
import de.osmembrane.model.algorithms.GraphPlanarizer;
import de.osmembrane.model.algorithms.TarjanAlgorithm;
import de.osmembrane.model.parser.ParserFactory;
import de.osmembrane.model.persistence.AbstractPersistence;
import de.osmembrane.model.persistence.FileException;
import de.osmembrane.model.persistence.FileType;
import de.osmembrane.model.persistence.OSMembranePersistence;
import de.osmembrane.model.persistence.PersistenceFactory;
import de.osmembrane.model.persistence.PipelinePersistenceObject;
import de.osmembrane.model.pipeline.PipelineObserverObject.ChangeType;
import de.osmembrane.model.settings.SettingType;
import de.osmembrane.resources.Constants;
import de.osmembrane.tools.Tools;

/**
 * Implementation of {@link AbstractPipeline}.
 * 
 * @author jakob_jarosch
 */
public class Pipeline extends AbstractPipeline {

    private Stack<PipelineMemento> undoStack;
    private PipelineMemento currentState;
    private Stack<PipelineMemento> redoStack;

    private List<AbstractFunction> functions;

    private URL pipelineFilename;

    private AbstractPipelineSettings pipelineSettings;

    private boolean savedState;

    /**
     * Says if the pipeline is silent or not.<br/>
     * In the silent-mode the pipeline will not inform any observers.
     */
    private boolean silent;

    /**
     * Says if the pipeline uses undo-redo or not.<br/>
     * in disabled mode the pipeline does not save any undo redo steps.
     */
    private boolean undoRedoDisabled;

    /**
     * Creates a default pipeline with<br/>
     * 
     * @link {@link Pipeline#silent} = false
     */
    public Pipeline() {
        this(false);
    }

    /**
     * Creates a default pipeline with<br/>
     * 
     * @link {@link Pipeline#undoRedoDisabled} = false
     */
    public Pipeline(boolean silent) {
        this(silent, false);
    }

    /**
     * Constructor for {@link Pipeline}.
     */
    public Pipeline(boolean silent, boolean undoRedoDisabled) {
        this.functions = new ArrayList<AbstractFunction>();
        this.undoStack = new Stack<PipelineMemento>();
        this.redoStack = new Stack<PipelineMemento>();
        this.silent = silent;
        this.undoRedoDisabled = undoRedoDisabled;
        this.savedState = true;
        this.pipelineFilename = null;
        this.pipelineSettings = new PipelineSettings();

        /* register the Observer of Persistence to the Pipeline */
        addObserver(PersistenceFactory.getInstance());
    }

    @Override
    public AbstractPipelineSettings getSettings() {
        return pipelineSettings;
    }

    @Override
    public void clear() {
        this.functions.clear();
        this.undoStack.clear();
        this.redoStack.clear();
        pipelineSettings = new PipelineSettings();
        pipelineFilename = null;

        changeSavedState(true);

        /* notify the observers */
        changedNotifyObservers(new PipelineObserverObject(
                ChangeType.FULLCHANGE, null).setCreateUndoStep(false));
    }

    @Override
    public AbstractFunction[] getFunctions() {
        AbstractFunction[] functions = new AbstractFunction[this.functions
                .size()];
        functions = this.functions.toArray(functions);
        return functions;
    }

    @Override
    public void addFunction(AbstractFunction func) {
        func.setPipeline(this);
        func.addObserver(this);
        functions.add(func);

        /* notify the observers */
        changedNotifyObservers(new PipelineObserverObject(
                ChangeType.ADD_FUNCTION, func));
    }

    @Override
    public boolean deleteFunction(AbstractFunction func) {
        boolean returnValue = false;

        for (AbstractFunction function : functions) {
            if (function == func) {
                function.unlinkConnectors();
                returnValue = functions.remove(function);
                break;
            }
        }

        if (returnValue == true) {
            /* notify the observers */
            changedNotifyObservers(new PipelineObserverObject(
                    ChangeType.DELETE_FUNCTION, func));
        }

        return returnValue;
    }

    @Override
    public boolean isComplete() {
        /* check all functions */
        for (AbstractFunction function : getFunctions()) {
            if (!function.isComplete()) {
                return false;
            }
        }

        /* all functions seems to be complete, so the pipeline is also complete. */
        return true;
    }

    @Override
    public void loadPipeline(URL filename) throws FileException {
        AbstractPersistence persistence = PersistenceFactory.getInstance()
                .getPersistence(OSMembranePersistence.class);

        PipelinePersistenceObject pipeline = (PipelinePersistenceObject) persistence
                .load(filename);

        clear();

        pipelineFilename = filename;
        this.functions = pipeline.getFunctions();
        for (AbstractFunction function : functions) {
            function.addObserver(this);
        }

        this.pipelineSettings = pipeline.getSettings();

        changeSavedState(true);

        /* notify the observers */
        changedNotifyObservers(new PipelineObserverObject(
                ChangeType.FULLCHANGE, null).setCreateUndoStep(false));
    }

    @Override
    public void savePipeline() throws FileException {
        savePipeline(pipelineFilename);
    }

    @Override
    public void savePipeline(URL filename) throws FileException {
        AbstractPersistence persistence = PersistenceFactory.getInstance()
                .getPersistence(OSMembranePersistence.class);

        persistence.save(filename, new PipelinePersistenceObject(functions,
                pipelineSettings));
        pipelineFilename = filename;

        /* Saved successfully (persistence has not thrown a FileException */
        changeSavedState(true);

        changedNotifyObservers(new PipelineObserverObject(
                ChangeType.SAVED_PIPELINE, null).setCreateUndoStep(false));
    }

    @Override
    public boolean isSaved() {
        return savedState;
    }

    @Override
    public URL getFilename() {
        return pipelineFilename;
    }

    @Override
    public void backupPipeline() throws FileException {
        AbstractPersistence persistence = PersistenceFactory.getInstance()
                .getPersistence(OSMembranePersistence.class);

        persistence.save(Constants.DEFAULT_BACKUP_FILE,
                new PipelinePersistenceObject(functions, pipelineSettings));
    }

    @Override
    public boolean isBackupAvailable() {
        File file = Tools.urlToFile(Constants.DEFAULT_BACKUP_FILE);
        return file.isFile();
    }

    @Override
    public void loadBackup() throws FileException {
        loadPipeline(Constants.DEFAULT_BACKUP_FILE);
    }

    @Override
    public void clearBackup() {
        if (isBackupAvailable()) {
            Tools.urlToFile(Constants.DEFAULT_BACKUP_FILE).delete();
        }
    }

    @Override
    public void importPipeline(URL filename, FileType type)
            throws FileException {
        AbstractPersistence persistence = PersistenceFactory.getInstance()
                .getPersistence(type.getPersistenceClass());

        PipelinePersistenceObject pipeline = (PipelinePersistenceObject) persistence
                .load(filename);

        clear();

        this.functions = pipeline.getFunctions();
        for (AbstractFunction function : functions) {
            function.addObserver(this);
        }

        this.pipelineSettings = pipeline.getSettings();

        /* notify the observers */
        changedNotifyObservers(new PipelineObserverObject(
                ChangeType.FULLCHANGE, null));
    }

    @Override
    public String generate(FileType filetype) {
        return ParserFactory
                .getInstance()
                .getParser(filetype.getParserClass())
                .parsePipeline(
                        new PipelinePersistenceObject(functions,
                                pipelineSettings));
    }

    @Override
    public void exportPipeline(URL filename, FileType type)
            throws FileException {
        AbstractPersistence persistence = PersistenceFactory.getInstance()
                .getPersistence(type.getPersistenceClass());

        persistence.save(filename, new PipelinePersistenceObject(functions,
                pipelineSettings));
    }

    @Override
    public boolean hasLoop() {
        TarjanAlgorithm check = new TarjanAlgorithm(functions);
        check.run();

        /* check the SCCs */
        List<List<AbstractFunction>> sccs = check.getSCC();

        for (List<AbstractFunction> scc : sccs) {
            if (scc.size() == 1) {
                /*
                 * check if the scc with size 1 links to itself or is just
                 * standing alone
                 */
                AbstractFunction function = scc.get(0);
                for (AbstractConnector outConnector : function
                        .getOutConnectors()) {
                    for (AbstractConnector inConnector : outConnector
                            .getConnections()) {
                        if (inConnector.getParent() == function) {
                            /* found a connection to the function itself */
                            return true;
                        }
                    }
                }
            } else if (scc.size() > 1) {
                return true;
            }
        }

        return false;
    }

    @Override
    public void arrangePipeline() {
        GraphPlanarizer gprizer = new GraphPlanarizer(functions);
        gprizer.planarize();
        changedNotifyObservers(new PipelineObserverObject(
                ChangeType.FULLCHANGE, null));
    }

    @Override
    public boolean undo() {
        if (!undoAvailable()) {
            return false;
        }

        redoStack.push(currentState);
        restoreMemento(undoStack.pop());

        return true;
    }

    @Override
    public boolean undoAvailable() {
        return !undoStack.isEmpty();
    }

    @Override
    public boolean redo() {
        if (!redoAvailable()) {
            return false;
        }

        undoStack.push(currentState);
        restoreMemento(redoStack.pop());

        return true;
    }

    @Override
    public boolean redoAvailable() {
        return !redoStack.isEmpty();
    }

    @Override
    public void update(Observable o, Object arg) {
        /* only PipelineObserverObjects will be passed through */
        if (arg instanceof PipelineObserverObject) {
            changedNotifyObservers((PipelineObserverObject) arg);
        }
    }

    @Override
    protected void changedNotifyObservers(PipelineObserverObject poo) {
        poo.setPipeline(this);

        /* check if the undo-step is really required, or disabled. */
        if (poo.createUndoStep() && !undoRedoDisabled) {
            /* any changes made, set savedState to false */
            changeSavedState(false);
        }

        if (!silent) {
            this.setChanged();
            this.notifyObservers(poo);
        }
    }

    private void changeSavedState(boolean state) {
        this.savedState = state;

        if (state == false) {
            saveStep();
        } else {
            /*
             * Update the savedState for the current item (nothing changed in
             * the pipeline only the state should be updated.
             */
            currentState = new PipelineMemento(functions, savedState);
        }
    }

    private void saveStep() {
        undoStack.push(currentState);
        redoStack.clear();
        currentState = new PipelineMemento(functions, savedState);

        int maximumStackSize = ((Integer) ModelProxy.getInstance()
                .getSettings().getValue(SettingType.MAXIMUM_UNDO_STEPS))
                .intValue();

        while (undoStack.size() > maximumStackSize) {
            undoStack.remove(0);
        }
    }

    private void restoreMemento(PipelineMemento memento) {
        this.functions = memento.getFunctions();
        this.savedState = memento.getSavedState();
        this.currentState = memento;

        for (AbstractFunction function : functions) {
            function.addObserver(this);
        }

        changedNotifyObservers(new PipelineObserverObject(
                ChangeType.FULLCHANGE, null).setCreateUndoStep(false));
    }
}

/**
 * Private internal class of the memento of the undo-/redo-feature.
 * 
 * @author jakob_jarosch
 */
class PipelineMemento {

    private List<AbstractFunction> functions = new ArrayList<AbstractFunction>();
    private boolean savedState;

    public PipelineMemento(List<AbstractFunction> functions, boolean savedState) {
        this.functions = deepCopyFunctions(functions);
        this.savedState = savedState;
    }

    public boolean getSavedState() {
        return savedState;
    }

    public List<AbstractFunction> getFunctions() {
        return deepCopyFunctions(functions);
    }

    private List<AbstractFunction> deepCopyFunctions(
            List<AbstractFunction> functions) {
        /* Use serialization to create a copy of the functions in the pipeline */
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(functions);
            ByteArrayInputStream bais = new ByteArrayInputStream(
                    baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            Object deepCopy = ois.readObject();

            @SuppressWarnings("unchecked")
            List<AbstractFunction> copy = (List<AbstractFunction>) deepCopy;
            return copy;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        return functions;
    }
}
