package be.iminds.ilabt.jfed.experimenter_gui.editor;

import be.iminds.ilabt.jfed.experimenter_gui.config.JFedGuiConfig;
import be.iminds.ilabt.jfed.experimenter_gui.editor.actions.EditorActionsFactory;
import be.iminds.ilabt.jfed.experimenter_gui.editor.rspec_validation.RspecCheckReason;
import be.iminds.ilabt.jfed.experimenter_gui.editor.rspec_validation.RspecValidationEngine;
import be.iminds.ilabt.jfed.experimenter_gui.editor.rspec_validation.RspecValidationResult;
import be.iminds.ilabt.jfed.experimenter_gui.editor.views.EditableCanvasEditorView;
import be.iminds.ilabt.jfed.experimenter_gui.editor.views.EditableRspecView;
import be.iminds.ilabt.jfed.experimenter_gui.editor.views.EditorViewType;
import be.iminds.ilabt.jfed.experimenter_gui.editor.views.EditorViewsFactory;
import be.iminds.ilabt.jfed.experimenter_gui.slice.ModelSliceView;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.rspec.model.ModelRspecType;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.FXModelRspec;
import be.iminds.ilabt.jfed.rspec.rspec_source.RequestRspecSource;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.layout.BorderPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.util.concurrent.FutureTask;

/**
 * User: twalcari
 * Date: 11/5/13
 * Time: 6:24 PM
 */
public class ExperimentEditor extends BorderPane {
    private static final Logger LOG = LoggerFactory.getLogger(ExperimentEditor.class);

    private final EditableRspec editableRspec;

    private final TestbedInfoSource testbedInfoSource;
    private final JFedGuiConfig config;
    private final EditorViewsFactory editorViewsFactory;
    private final EditorActionsFactory editorActionsFactory;

    private final StringProperty statusProperty = new SimpleStringProperty();
    private final RspecValidationEngine validationEngine = new RspecValidationEngine();

    private EditableRspecView currentEditableRspecView;

    private boolean switchingView = false; //flag to prevent unnecessary refresh of view when switching views

    /**
     * this variable contains the current model(editor) when the canvas or timeline is visible
     */
    @Nullable
    private ModelRspecEditor modelRspecEditor;

    /**
     * This starts the experiment editor. If the rspecSource can be succesfully parsed, it opens in graphical mode.
     * Otherwise, it opens in the RSpec-editor-view.
     */
    public ExperimentEditor(EditableRspec editableRspec, TestbedInfoSource testbedInfoSource, JFedGuiConfig config,
                            EditorViewsFactory editorViewsFactory, EditorActionsFactory editorActionsFactory) {
        super();

        this.editableRspec = editableRspec;

        this.testbedInfoSource = testbedInfoSource;
        this.config = config;
        this.editorViewsFactory = editorViewsFactory;
        this.editorActionsFactory = editorActionsFactory;

        //make sure that a new ModelRspecEditor-object is created each time when the source changes
        updateModelRspecEditor();
        editableRspec.requestRspecSourceProperty().addListener(observable -> updateModelRspecEditor());

        //initialize GUI
        if (modelRspecEditor == null) {
            //there is an error in the RSpec preventing it from being converted to a model,
            // so we show the raw RSpec XML and continue
            setEditorViewByType(EditorViewType.RAW);
        } else {
            if (!editableRspec.getRequestRspecSource().isXmlBased()) {
                setEditorViewByType(EditorViewType.CANVAS);

                //rearrange nodes on circle if not all positions are defined
                if (!ModelSliceView.areAllNodePositionsDefined(modelRspecEditor.getModelRspec())) {
                    ((EditableCanvasEditorView) currentEditableRspecView).autoLayout();
                }


            } else {
                setEditorViewByType(EditorViewType.RAW);
            }

            //show unbound nodes dialog if necessary
            Platform.runLater(new FutureTask<>(
                    editorActionsFactory.createProcessUnboundNodesAction(editableRspec, null)));
        }

        /* when the RequestRspecSource changes later on, we must make sure that this is automatically reflected
         in the editor-view which is visible at that time. Even if this incurs data loss! */

        editableRspec.requestRspecSourceProperty().addListener(observable -> {

            synchronized (ExperimentEditor.this) {
                if(!switchingView) {
                    //this will force a new view to be constructed with the changed RequestRspecSource
                    setEditorViewByType(currentEditableRspecView.getType());
                }
            }
        });
    }

    private void updateModelRspecEditor() {
        FXModelRspec modelRspec = editableRspec.getRequestRspecSource().getModelRspec(ModelRspecType.FX);
        if (modelRspec != null) {
            modelRspecEditor = new ModelRspecEditor(modelRspec, testbedInfoSource, config);
        } else {
            modelRspecEditor = null;
        }
    }

    private void setEditorViewByType(EditorViewType type) {
        if (currentEditableRspecView != null) {
            currentEditableRspecView.dispose();
        }

        if (type == EditorViewType.CANVAS) {
            assert modelRspecEditor != null;
            currentEditableRspecView = editorViewsFactory.createEditableCanvasEditorView(modelRspecEditor);

        } else if (type == EditorViewType.RAW) {
            currentEditableRspecView =
                    editorViewsFactory.createRawRspecEditor(editableRspec.getRequestRspecSource().getStringRspec());
        } else if (type == EditorViewType.CONTROLLER) {
            assert modelRspecEditor != null;
            currentEditableRspecView = editorViewsFactory.createControllerUI(modelRspecEditor);
        }

        assert (currentEditableRspecView != null);
        setCenter(currentEditableRspecView.getView());

        statusProperty.unbind();
        statusProperty.bind(currentEditableRspecView.statusProperty());
    }

    /**
     * Changes the view to the requested type, while retaining all information from the previous view.
     * <p>
     * We assume that getRequestRspec() of the previous view will return a valid Rspec for this new view type!
     *
     * @param type new EditorViewType to switch to
     */
    public void switchToEditorViewByType(EditorViewType type) {
        assert (currentEditableRspecView != null);

        synchronized (this) {
            switchingView = true;

            if (currentEditableRspecView.getType() == type)
                return; //we are already in the correct mode

            editableRspec.setRequestRspecSource(currentEditableRspecView.getRequestRspecSource());

            setEditorViewByType(type);

            switchingView = false;
        }
    }

    /**
     * Tries to the view to the requested type, without losing any data during the switch.
     * <p>
     * Before making the switch, this method will first check if data will be lost.
     * If so, the user will be warned, and can abort the process.
     *
     * @param type new EditorViewType to switch to
     * @return If switch was a success
     */
    public boolean trySwitchToEditorViewByType(EditorViewType type) {
        assert (currentEditableRspecView != null);

        if (currentEditableRspecView.getType() == type)
            return true; //we are already in the correct mode

        //we only need to check for data loss when switching from RAW
        if (currentEditableRspecView.getType() == EditorViewType.RAW) {

            final RequestRspecSource currentRequestRspecSource = getCurrentRequestRspecSource();

            RspecValidationResult validationResult =
                    validationEngine.validateRSpec(RspecCheckReason.SWITCH_TO_VISUAL_EDITOR,
                            currentRequestRspecSource, getScene().getWindow());

            if (validationResult != null) {
                if (validationResult.isAbort())
                    return false;

                editableRspec.setRequestRspecSource(validationResult.getReplacementRequestRspecSource());
            }
        }

        switchToEditorViewByType(type);

        return true;
    }


    public EditorViewType getCurrentEditorViewType() {
        return currentEditableRspecView.getType();
    }

    public void setStatus(String status) {
        statusProperty.set(status);
    }


    public RequestRspecSource getCurrentRequestRspecSource() {
        return currentEditableRspecView.getRequestRspecSource();
    }

    public StringProperty statusProperty() {
        return statusProperty;
    }

    public void save() {
        flushCurrentRequestRspecSource();
        editorActionsFactory.createSaveRspecAction(editableRspec, getScene().getWindow()).call();
    }

    public EditableRspec getEditableRspec() {
        return editableRspec;
    }

    public EditableRspecView getCurrentEditableRspecView() {
        return currentEditableRspecView;
    }

    public void startExperiment() {
        flushCurrentRequestRspecSource();
        editorActionsFactory.createStartExperimentAction(editableRspec, getScene().getWindow()).call();

    }

    public void createReservation() {
        flushCurrentRequestRspecSource();
        editorActionsFactory.createCreateReservationAction(editableRspec, getScene().getWindow()).call();
    }

    private void flushCurrentRequestRspecSource(){
        editableRspec.setRequestRspecSource(getCurrentRequestRspecSource());
    }

    public boolean isDirty() {
        flushCurrentRequestRspecSource();
        return editableRspec.isDirty();
    }
}
