package com.google.research.bleth.simulator;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableMap.toImmutableMap;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.common.collect.ImmutableMap;
import com.google.research.bleth.exceptions.StatisticsAlreadyExistException;
import java.util.Map;

/** A mediator between the statistics the simulation gathered and their storage on datastore. */
public class StatisticsState {
    private final String simulationId;
    private final  Map<String, Double> distanceStats;
    private final Map<String, Double> beaconsObservedPercent;

    /**
     * Create a new StatisticsState.
     * @param simulationId is the simulation id associated with the statistical data.
     * @param distanceStats is statistics about the difference between the beacons' real locations and their estimated locations.
     * @param beaconsObservedPercent maps each beacon to the percentage of rounds in which it has been observed.
     * @return a new instance of StatisticsState
     */
    public static StatisticsState create(String simulationId, Map<String, Double> distanceStats, Map<String, Double> beaconsObservedPercent) {
        checkNotNull(distanceStats);
        checkNotNull(beaconsObservedPercent);
        return new StatisticsState(simulationId, ImmutableMap.copyOf(distanceStats), ImmutableMap.copyOf(beaconsObservedPercent));
    }

    /**
     * Create and write multiple datastore entities, each represents an aggregate function such as min and max,
     * of the distance between the beacons' real locations and their estimated locations.
     * @throws StatisticsAlreadyExistException if the simulation's distance statistics are already exists in the database.
     */
    public void writeDistancesStats() {
        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

        Map<String, Double> distanceStatsInDb = readDistancesStats(simulationId);
        if (!distanceStatsInDb.isEmpty()) {
            throw new StatisticsAlreadyExistException(simulationId);
        }

        Entity entity = new Entity(Schema.StatisticsState.entityKindDistance);
        entity.setProperty(Schema.StatisticsState.simulationId, simulationId);
        for (Map.Entry<String, Double> entry : distanceStats.entrySet()) {
            entity.setProperty(entry.getKey(), entry.getValue());
        }
        datastore.put(entity);
    }

    /**
     * Create and write multiple datastore entities, each represents the percentage of rounds a beacon
     * has been observed by at least one observer, i.e. has been detected by the resolver.
     * @throws StatisticsAlreadyExistException if the simulation's distance statistics are already exists in the database.
     */
    public void writeBeaconsObservedPercentStats() {
        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

        Map<String, Double> beaconsIdToObservedPercent = readBeaconsObservedPercentStats(simulationId);
        if (!beaconsIdToObservedPercent.isEmpty()) {
            throw new StatisticsAlreadyExistException(simulationId);
        }

        Entity entity = new Entity(Schema.StatisticsState.entityKindBeaconsObservedPercent);
        entity.setProperty(Schema.StatisticsState.simulationId, simulationId);
        for (Map.Entry<String, Double> entry : beaconsObservedPercent.entrySet()) {
            entity.setProperty(entry.getKey(), entry.getValue());
        }
        datastore.put(entity);
    }

    /**
     * Read from the db statical data about the distance between the beacons' real locations and their estimated locations.
     * @param simulationId is the simulation id associated with the statistical data.
     * @return a map that maps an aggregate function to its value on the simulation.
     */
    public static Map<String, Double> readDistancesStats(String simulationId) {
        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

        Query.FilterPredicate filterBySimulationId =
                new Query.FilterPredicate(Schema.StatisticsState.simulationId, Query.FilterOperator.EQUAL, simulationId);
        Query distances = new Query(Schema.StatisticsState.entityKindDistance).setFilter(filterBySimulationId);
        Entity distancesStatistics = datastore.prepare(distances).asSingleEntity();

        if (distancesStatistics == null) {
            return ImmutableMap.of();
        }

        return distancesStatistics.getProperties().entrySet().stream()
                .filter(entry -> !(entry.getKey().equals(Schema.StatisticsState.simulationId)))
                .collect(toImmutableMap(e -> e.getKey(), e -> (Double) e.getValue()));
    }

    /**
     * Read from the db statical data about the percentage of rounds each beacon has been observed.
     * @param simulationId is the simulation id associated with the statistical data.
     * @return a map that maps a beacon's id to the percentage of rounds a beacon has been observed by at least one observer.
     */
    public static Map<String, Double> readBeaconsObservedPercentStats(String simulationId) {
        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

        Query.FilterPredicate filterBySimulationId =
                new Query.FilterPredicate(Schema.StatisticsState.simulationId, Query.FilterOperator.EQUAL, simulationId);
        Query percents = new Query(Schema.StatisticsState.entityKindBeaconsObservedPercent).setFilter(filterBySimulationId);
        Entity percentsStatistics = datastore.prepare(percents).asSingleEntity();

        if (percentsStatistics == null) {
            return ImmutableMap.of();
        }

        return percentsStatistics.getProperties().entrySet().stream()
                .filter(entry -> !(entry.getKey().equals(Schema.StatisticsState.simulationId)))
                .collect(toImmutableMap(e -> e.getKey(), e -> (Double) e.getValue()));
    }

    private StatisticsState(String simulationId, Map<String, Double> distanceStats, Map<String, Double> beaconsObserved) {
        this.simulationId = simulationId;
        this.distanceStats = distanceStats;
        this.beaconsObservedPercent = beaconsObserved;
    }
}

@@ -1711,32 +1711,25 @@
 e and write 
-multiple
+a
  datastore e
@@ -1728,33 +1728,26 @@
 astore entit
-ies, each
+y,
  represents 
@@ -1746,19 +1746,16 @@
 resents 
-an 
 aggregat
@@ -1764,16 +1764,17 @@
 function
+s
  such as
@@ -2722,16 +2722,9 @@
 ite 
-multiple
+a
  dat
@@ -2739,17 +2739,10 @@
 ntit
-ies, each
+y,
  rep
@@ -3999,18 +3999,20 @@
 at maps 
-an
+each
  aggrega

Update javadoc.