package be.iminds.jfed.gts_highlevel.controller;

import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.JFedPasswordManager;
import be.iminds.jfed.gts_highlevel.model.*;
import com.google.common.collect.Maps;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.*;
import java.util.stream.Collectors;

/**
 * User: twalcari
 * Date: 9/24/2015
 * Time: 15:48
 */
@Singleton
public class GtsModel {
    private static final Logger LOG = LoggerFactory.getLogger(GtsModel.class);
    private final JFedPasswordManager passwordManager;

    private final ReadOnlyObjectWrapper<GtsCredentials> credentials = new ReadOnlyObjectWrapper<>();
    private final ObservableMap<String, GtsXmlProject> projects = FXCollections.observableHashMap();
    private final ObservableMap<String, GtsInfrastructure> infrastructures = FXCollections.observableHashMap();
    private final ObservableMap<Integer, GtsXmlReservation> reservations = FXCollections.observableHashMap();
    private final ObservableMap<String, GtsXmlResource> resources = FXCollections.observableHashMap();
    private final ObservableMap<TypeId, GtsXmlType> types = FXCollections.observableHashMap();

    private final ObservableMap<Integer, TypeId> reservationsByTypeId = FXCollections.observableHashMap();

    //create strong referenced readonly-wrappers
    private final ObservableMap<String, GtsXmlProject> readOnlyProjects =
            FXCollections.unmodifiableObservableMap(projects);
    private final ObservableMap<String, GtsInfrastructure> readOnlyInfrastructures =
            FXCollections.unmodifiableObservableMap(infrastructures);
    private final ObservableMap<Integer, GtsXmlReservation> readOnlyReservations =
            FXCollections.unmodifiableObservableMap(reservations);
    private final ObservableMap<String, GtsXmlResource> readOnlyResources =
            FXCollections.unmodifiableObservableMap(resources);
    private final ObservableMap<TypeId, GtsXmlType> readOnlyTypes = FXCollections.unmodifiableObservableMap(types);

    private final ObservableMap<Integer, TypeId> readOnlyReservationsByTypeId =
            FXCollections.unmodifiableObservableMap(reservationsByTypeId);


    @Inject
    public GtsModel(JFedPasswordManager passwordManager,
                    JFedPreferences jFedPreferences,
                    GeniUserProvider geniUserProvider) {
        this.passwordManager = passwordManager;

        passwordManager.load(jFedPreferences, geniUserProvider.getLoggedInGeniUser());

        resources.addListener((MapChangeListener<String, GtsXmlResource>) change -> LOG.warn("Change to resources-map: {}", change));
    }


    public GtsCredentials getCredentials() {
        return credentials.get();
    }

    public ReadOnlyObjectProperty<GtsCredentials> credentialsProperty() {
        return credentials.getReadOnlyProperty();
    }

    public void setCredentials(GtsCredentials credentials) {
        this.credentials.set(credentials);
    }

    public String getUsername() {
        return passwordManager.getLogins("gts").get(0).getUsername();
    }

    public ObservableMap<String, GtsXmlProject> getProjects() {
        return readOnlyProjects;
    }

    void addOrUpdateProject(GtsXmlProject project) {
        projects.put(project.getId(), project);
    }

    /**
     * Register a project as removed
     */
    void removeProject(GtsXmlProject project) {
        projects.remove(project.getId());
    }

    public ObservableMap<String, GtsInfrastructure> getInfrastructures() {
        return readOnlyInfrastructures;
    }

    void addOrUpdateInfrastructure(GtsInfrastructure infrastructure) {
        infrastructures.put(infrastructure.getProviderId(), infrastructure);
    }

    /**
     * Register a infrastructure as removed
     */
    void removeInfrastructure(GtsInfrastructure infrastructure) {
        infrastructures.remove(infrastructure.getProviderId());
    }

    public ObservableMap<Integer, GtsXmlReservation> getReservations() {
        return readOnlyReservations;
    }


    GtsXmlReservation addOrUpdateReservation(GtsXmlReservation newReservation) {
        List<GtsXmlReservation> newChildrenList = newReservation.getChildren().stream()
                .map(this::addOrUpdateReservation)
                .collect(Collectors.toList());

        newReservation.setChildren(newChildrenList);

        return reservations.merge(newReservation.getId(), newReservation, (existingReservation, newReservation1) -> {
            GtsXmlReservation.copy(existingReservation, newReservation1);
            return existingReservation;
        });
    }

    /**
     * Register a reservation as removed
     */
    synchronized void removeReservation(GtsXmlReservation reservation) {
        LOG.warn("Removing reservation {}", reservation.getId());
        GtsXmlReservation removedReservation = reservations.remove(reservation.getId());
        if (removedReservation != null)
            removedReservation.setStatus(GtsXmlReservation.Status.DELETED);
    }


    public ObservableMap<String, GtsXmlResource> getResources() {
        return readOnlyResources;
    }

    public synchronized Set<GtsXmlResource> getResourcesInReservation(GtsXmlReservation reservation) {
        assert reservations.containsValue(reservation) || reservation.getStatus() == GtsXmlReservation.Status.DELETED :
                "Reservation " + reservation + " could not be found in the model";

        Set<GtsXmlResource> result = new HashSet<>();
        getResourcesInReservation(reservation, result);
        return result;
    }

    private void getResourcesInReservation(GtsXmlReservation reservation, Set<GtsXmlResource> result) {
        if (resources.containsKey(reservation.getResourcesProviderId())) {
            result.add(resources.get(reservation.getResourcesProviderId()));
        }

        reservation.getChildren().forEach(childRes -> getResourcesInReservation(childRes, result));
    }


    void addOrUpdateResource(GtsXmlResource newResource) {
        newResource.setReservations(addOrUpdateReservation(newResource.getReservations()));

        resources.merge(newResource.getProviderId(), newResource, (existingResource, newResource1) -> {
            GtsXmlResource.copy(existingResource, newResource1);
            return existingResource;
        });

        LOG.debug("Saw resource {}. Model now has {} in total", newResource, resources.size());
    }

    /**
     * Register a resource as removed
     */
    void removeResource(GtsXmlResource resource) {
        final GtsXmlResource removedResource = resources.remove(resource.getProviderId());
        if (removedResource != null)
            removedResource.setStatus(GtsXmlReservation.Status.DELETED);
    }


    public ObservableMap<TypeId, GtsXmlType> getTypes() {
        return readOnlyTypes;
    }

    public Map<TypeId, GtsXmlType> getTypesInProject(GtsXmlProject project) {
        return Maps.filterEntries(types, input -> Objects.equals(input.getKey().projectName, project.getId()));
    }


    void addOrUpdateType(GtsXmlType type, GtsXmlProject project) {
        types.put(new TypeId(project.getId(), type.getName()), type);
    }

    /**
     * Register a type as removed
     */
    boolean removeType(GtsXmlType type, GtsXmlProject project) {
        return types.remove(new TypeId(project.getId(), type.getName())) != null;
    }

    /**
     * Register a type as removed
     */
    boolean removeType(String typeName, GtsXmlProject project) {
        return types.remove(new TypeId(project.getId(), typeName)) != null;
    }

    boolean removeProject(String projectName) {
        return projects.remove(projectName) != null;
    }

    synchronized boolean removeReservation(int reservationId) {
        final GtsXmlReservation reservation = reservations.remove(reservationId);
        if (reservation != null)
            reservation.setStatus(GtsXmlReservation.Status.DELETED);
        return reservation != null;
    }

    @Nullable
    public GtsXmlReservation getReservation(int reservationId) {
        return reservations.get(reservationId);
    }


    public void registerReservationOfType(Integer reservationId, TypeId typeId) {
        reservationsByTypeId.put(reservationId, typeId);
    }

    public void deregisterReservationOfType(Integer reservationId) {
        reservationsByTypeId.remove(reservationId);
    }

    public ObservableMap<Integer, TypeId> getReservationsByTypeId() {
        return readOnlyReservationsByTypeId;
    }

    public final static class TypeId {
        private final String projectName;
        private final String typeName;

        public TypeId(@Nonnull String projectName, @Nonnull String typeName) {
            this.projectName = projectName;
            this.typeName = typeName;
        }

        public String getProjectName() {
            return projectName;
        }

        public String getTypeName() {
            return typeName;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            TypeId typeId = (TypeId) o;

            if (!Objects.equals(projectName, typeId.projectName)) return false;
            return Objects.equals(typeName, typeId.typeName);

        }

        @Override
        public int hashCode() {
            int result = projectName.hashCode();
            result = 31 * result + typeName.hashCode();
            return result;
        }

        @Override
        public String toString() {
            return "TypeId{" +
                    "projectName='" + projectName + '\'' +
                    ", typeName='" + typeName + '\'' +
                    '}';
        }
    }

}
