/* Copyright (c) 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.sample.mobileassistant;

import static com.google.appengine.api.utils.SystemProperty.Environment.Value.Development;
import static com.google.appengine.api.utils.SystemProperty.environment;

import com.google.appengine.api.datastore.GeoPt;
import com.google.appengine.api.search.Document;
import com.google.appengine.api.search.Field;
import com.google.appengine.api.search.GeoPoint;
import com.google.appengine.api.search.Index;
import com.google.appengine.api.search.IndexSpec;
import com.google.appengine.api.search.Query;
import com.google.appengine.api.search.QueryOptions;
import com.google.appengine.api.search.Results;
import com.google.appengine.api.search.ScoredDocument;
import com.google.appengine.api.search.SearchServiceFactory;
import com.google.appengine.api.search.SortExpression;
import com.google.appengine.api.search.SortOptions;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

/**
 * Helper class for geo-proximity related management of Places.
 */
class PlacesHelper {
  private static final Logger log = Logger.getLogger(PlacesHelper.class.getName());
  private static final String INDEX_NAME = "Places";

  static Index getIndex() {
    IndexSpec indexSpec = IndexSpec.newBuilder().setName(INDEX_NAME).build();
    return SearchServiceFactory.getSearchService().getIndex(indexSpec);
  }

  static Document buildDocument(
      String placeId, String placeName, String placeAddress, GeoPt location) {
    GeoPoint geoPoint = new GeoPoint(location.getLatitude(), location.getLongitude());

    Document.Builder builder = Document.newBuilder()
        .addField(Field.newBuilder().setName("id").setText(placeId))
        .addField(Field.newBuilder().setName("name").setText(placeName))
        .addField(Field.newBuilder().setName("address").setText(placeAddress))
        .addField(Field.newBuilder().setName("place_location").setGeoPoint(geoPoint));

    // geo-location doesn't work under dev_server, so let's add another
    // field to use for retrieving documents
    if (environment.value() == Development) {
      builder.addField(Field.newBuilder().setName("value").setNumber(1));
    }

    Document place = builder.build();

    return place;
  }

  static List<PlaceInfo> getPlaces(GeoPt location, long distanceInMeters, int resultCount) {

    // TODO(user): Use memcache

    String geoPoint = "geopoint(" + location.getLatitude() + ", " + location.getLongitude() + ")";

    String query = "distance(place_location, " + geoPoint + ") < " + distanceInMeters;
    String locExpr = "distance(place_location, " + geoPoint + ")";

    SortExpression sortExpr = SortExpression.newBuilder()
        .setExpression(locExpr)
        .setDirection(SortExpression.SortDirection.ASCENDING)
        .setDefaultValueNumeric(distanceInMeters + 1)
        .build();
    Query searchQuery = Query.newBuilder().setOptions(QueryOptions.newBuilder()
        .setSortOptions(SortOptions.newBuilder().addSortExpression(sortExpr))).build(query);
    Results<ScoredDocument> results = getIndex().search(searchQuery);

    if (results.getNumberFound() == 0) {
      // geo-location doesn't work under dev_server
      if (environment.value() == Development) {
        // return all documents
        results = getIndex().search("value > 0");
      }
    }

    List<PlaceInfo> places = new ArrayList<PlaceInfo>();

    for (ScoredDocument document : results) {
      if (places.size() >= resultCount) {
        break;
      }

      GeoPoint p = document.getOnlyField("place_location").getGeoPoint();

      PlaceInfo place = new PlaceInfo();
      place.setplaceID(document.getOnlyField("id").getText());
      place.setName(document.getOnlyField("name").getText());
      place.setAddress(document.getOnlyField("address").getText());

      place.setLocation(new GeoPt((float) p.getLatitude(), (float) p.getLongitude()));

      // GeoPoints are not implemented on dev server and latitude and longitude are set to zero
      // But since those are doubles let's play safe
      // and use double comparison with epsilon set to 0.0001
      if (Math.abs(p.getLatitude()) <= 0.0001 && Math.abs(p.getLongitude()) <= 0.0001) {
        // set a fake distance of 5+ km
        place.setDistanceInKilometers(5 + places.size());
      } else {
        double distance = distanceInMeters / 1000;
        try {
          distance = getDistanceInKm(
              p.getLatitude(), p.getLongitude(), location.getLatitude(), location.getLongitude());
        } catch (Exception e) {
          log.warning("Exception when calculating a distance: " + e.getMessage());
        }

        place.setDistanceInKilometers(distance);
      }

      places.add(place);
    }

    return places;
  }

  static double getDistanceInKm(
      double latitude1, double longitude1, double latitude2, double longitude2) {
    final double earthRadius = 6378.1; // kilometers

    double lat1 = Math.toRadians(latitude1);
    double lat2 = Math.toRadians(latitude2);
    double long1 = Math.toRadians(longitude1);
    double long2 = Math.toRadians(longitude2);

    double dist = earthRadius * Math.acos(Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1)
        * Math.cos(lat2) * Math.cos(Math.abs(long1 - long2)));

    return dist;
  }
}