package be.iminds.ilabt.jfed.experiment;

import be.iminds.ilabt.jfed.experiment.util.ExperimentRestoreInformation;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.highlevel.model.InternalState;
import be.iminds.ilabt.jfed.highlevel.model.Slice;
import be.iminds.ilabt.jfed.highlevel.model.Sliver;
import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.rspec.model.DistributeSshKeypair;
import be.iminds.ilabt.jfed.rspec.model.ModelRspecType;
import be.iminds.ilabt.jfed.rspec.rspec_source.RequestRspecSource;
import be.iminds.ilabt.jfed.util.GeniUrn;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.security.KeyPair;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * User: twalcari
 * Date: 1/30/2015
 * Time: 13:24
 */
public class Experiment {
    private static final Logger LOG = LoggerFactory.getLogger(Experiment.class);

    /*
    Variables with basic information. These fields will always be set in the constructor
     */
    @Nonnull
    private final String name;
    @Nullable
    private final String subAuthName; //may be null

    @Nullable
    private final RequestRspecSource newRequestRspecSource;

    @Nullable
    private final RequestRspecSource existingRequestRspecSource;


    /**
     * No sub-second precision allowed (FEDIBBTDEV-738). Must always be truncated to second precision!
     */
    @Nullable
    private final Instant requestedStartTime;

    /**
     * No sub-second precision allowed (FEDIBBTDEV-738). Must always be truncated to second precision!
     */
    @Nullable
    private final Instant requestedEndTime;

    /**
     * UserSpecs that must be given access to the experiment resources
     */
    @Nullable
    private List<UserSpec> userSpecs;
    private final boolean requestSla;

    private ExperimentRestoreInformation experimentRestoreInformation;

    /*
    Variables which are subject to change troughout the lifecycle of an experiment
     */
    private final ReadOnlyObjectWrapper<Slice> slice = new ReadOnlyObjectWrapper<>();

    private final ReadOnlyObjectWrapper<ExperimentState> experimentState = new ReadOnlyObjectWrapper<>();

    private final ObservableList<ExperimentPart> experimentParts = FXCollections.observableArrayList();

    /*
    Listeners
     */
    private final List<ExperimentChangeListener> experimentChangeListeners = new ArrayList<>();

    private final ObservableMap<String, Object> extras = FXCollections.observableHashMap();

    private final ObservableMap<DistributeSshKeypair, KeyPair> keypairs = FXCollections.observableHashMap();

    //instance initializer
    {
        this.experimentState.addListener((observable, oldValue, newValue) -> experimentChangeListeners.forEach(ecl -> ecl.onExperimentStateChange(newValue)));

        this.experimentParts.addListener((ListChangeListener<ExperimentPart>) c -> {
            while (c.next()) {
                assert c.wasAdded() : "ExperimentParts should only be added!";
                for (ExperimentPart part : c.getAddedSubList()) {
                    experimentChangeListeners.forEach(ecl -> ecl.onExperimentPartAdded(part));
                }
            }
        });
    }


    /**
     * Constructor used when starting a new experiment in a new to-be-created slice
     */
    public Experiment(@Nonnull String name, @Nullable String subAuthName, @Nonnull RequestRspecSource newRequestRspecSource,
                      @Nullable Instant requestedStartTime, @Nonnull Instant requestedEndTime,
                      @Nullable List<UserSpec> userSpecs, boolean requestSla) {
        assert name != null;
        assert newRequestRspecSource != null;
        assert requestedEndTime != null;
        this.name = name;
        this.subAuthName = subAuthName;
        this.userSpecs = userSpecs;
        this.requestSla = requestSla;

        this.requestedStartTime =
                requestedStartTime != null ? requestedStartTime.truncatedTo(ChronoUnit.SECONDS) : null;
        this.requestedEndTime =
                requestedEndTime.truncatedTo(ChronoUnit.SECONDS); //No sub-second precision allowed (FEDIBBTDEV-738)

        this.newRequestRspecSource = newRequestRspecSource;
        this.existingRequestRspecSource = null;

        this.experimentState.setValue(ExperimentState.PENDING);
    }


    /**
     * Constructor used when starting a new experiment in an existing slice
     */
    public Experiment(@Nonnull Slice slice, @Nonnull RequestRspecSource newRequestRspecSource,
                      @Nullable Instant requestedStartTime, @Nonnull Instant requestedEndTime,
                      @Nullable List<UserSpec> userSpecs, @Nullable RequestRspecSource existingRequestRspecSource, boolean requestSla) {
        this.requestSla = requestSla;
        this.name = slice.getName();
        this.subAuthName = slice.getSubAuthorityName();

        this.requestedStartTime =
                requestedStartTime != null ? requestedStartTime.truncatedTo(ChronoUnit.SECONDS) : null;
        this.requestedEndTime =
                requestedEndTime.truncatedTo(ChronoUnit.SECONDS); //No sub-second precision allowed (FEDIBBTDEV-738)

        this.newRequestRspecSource = newRequestRspecSource;
        this.existingRequestRspecSource = existingRequestRspecSource;

        this.userSpecs = userSpecs;

        this.experimentState.setValue(ExperimentState.RESTORING);
        registerSlice(slice);
    }


    /**
     * Constructor used when restoring a slice.
     */
    public Experiment(@Nonnull Slice slice, @Nullable Instant requestedStartTime) {
        this.name = slice.getName();
        this.subAuthName = slice.getSubAuthorityName();


        this.newRequestRspecSource = null;

        RequestRspecSource existingRequestRspecSource = slice.getRequestRspec();
        if (existingRequestRspecSource == null && slice.getManifestRspec() != null) {
            existingRequestRspecSource =
                    new RequestRspecSource(slice.getManifestRspec().getStringRspec(), ModelRspecType.FX);
        }
        this.existingRequestRspecSource = existingRequestRspecSource;

        this.requestedStartTime = requestedStartTime; //to be fixed slice.getStartDate();
        this.requestedEndTime = null;


        this.userSpecs = null;
        this.requestSla = false;
        registerSlice(slice);

        this.experimentState.set(ExperimentState.RESTORING);

    }

    public void registerSlice(Slice slice) {
        if (this.slice.get() != null)
            throw new IllegalStateException("Slice is already set in Experiment");
        if (slice == null)
            throw new IllegalArgumentException("Slice is null");
        if (slice.getUrn() == null)
            throw new IllegalArgumentException("Slice has no URN");
        this.slice.setValue(slice);

        this.experimentRestoreInformation = new ExperimentRestoreInformation(slice.getUrn());
        experimentRestoreInformation.bindToExperiment(this);
    }

    public void updateUserSpecs(@Nonnull List<UserSpec> userSpecs){
        this.userSpecs = userSpecs;
    }


    public void addExperimentChangeListener(ExperimentChangeListener experimentChangeListener) {
        assert (!this.experimentChangeListeners.contains(experimentChangeListener));

        this.experimentChangeListeners.add(experimentChangeListener);
    }

    public void removeExperimentChangeListener(ExperimentChangeListener experimentChangeListener) {
        assert (this.experimentChangeListeners.contains(experimentChangeListener));

        this.experimentChangeListeners.remove(experimentChangeListener);
    }


    @Nonnull
    public String getName() {
        return name;
    }

    @Nullable
    public List<UserSpec> getUserSpecs() {
        return userSpecs;
    }

    public boolean isRequestSla() {
        return requestSla;
    }

    @Nullable
    public Instant getRequestedEndTime() {
        return requestedEndTime;
    }

    @Nullable
    public Instant getRequestedStartTime() {
        return requestedStartTime;
    }

    @Nullable
    public RequestRspecSource getNewRequestRspecSource() {
        return newRequestRspecSource;
    }

    @Nullable
    public Slice getSlice() {
        return slice.get();
    }

    public ReadOnlyObjectProperty<Slice> sliceProperty() {
        return slice.getReadOnlyProperty();
    }


    public ExperimentState getExperimentState() {
        return experimentState.get();
    }

    public ReadOnlyObjectProperty<ExperimentState> experimentStateProperty() {
        return experimentState.getReadOnlyProperty();
    }

    @Nullable
    public String getSubAuthName() {
        return subAuthName;
    }

    public boolean isReservationMade() {
        return experimentParts.stream()
                .anyMatch(experimentPart -> experimentPart.getState() == InternalState.RESERVED);
    }

    public ExperimentRestoreInformation getExperimentRestoreInformation() {
        return experimentRestoreInformation;
    }

    public void setExperimentState(ExperimentState experimentState) {
        LOG.debug("Setting state for experiment {} to {}", name, experimentState);
        this.experimentState.set(experimentState);

    }

    @Nullable
    public RequestRspecSource getExistingRequestRspecSource() {
        return existingRequestRspecSource;
    }

    @Nonnull
    public ObservableList<ExperimentPart> getParts() {
        return experimentParts;
    }

    public void addExperimentPart(ExperimentPart experimentPart) {
        assert experimentPart.getExperiment() == this;

        this.experimentParts.add(experimentPart);
    }

    @Nullable
    public SfaExperimentPart getPart(Server auth) {
        return experimentParts.stream()
                .filter(part -> part instanceof SfaExperimentPart &&
                        Objects.equals(((SfaExperimentPart) part).getConnectSfaAuthority(), auth))
                .map(SfaExperimentPart.class::cast)
                .findFirst().orElse(null);
    }

    @Nullable
    public ExperimentPart getPart(Sliver sliver) {
        return experimentParts.stream()
                .filter(part -> part instanceof SfaExperimentPart
                        && ((SfaExperimentPart) part).getSlivers().contains(sliver))
                .findFirst().orElse(null);
    }

    @Nullable
    public ExperimentPart getPart(GeniUrn sliverUrn) {
        for (ExperimentPart experimentPart : experimentParts) {
            if (experimentPart instanceof SfaExperimentPart) {
                SfaExperimentPart sfaExperimentPart = (SfaExperimentPart) experimentPart;

                for (Sliver sliver : sfaExperimentPart.getSlivers()) {
                    if (Objects.equals(sliver.getUrn(), sliverUrn))
                        return experimentPart;
                }
            }
        }
        return null;
    }


    public ObservableMap<String, Object> getExtras() {
        return extras;
    }

    public ObservableMap<DistributeSshKeypair, KeyPair> getKeypairs() {
        return keypairs;
    }
}
