/*
 * Copyright 2011 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.gwt.geolocation.client;

import com.google.gwt.core.client.Callback;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.PartialSupport;

/**
 * Implements the HTML5 Geolocation interface.
 * 
 * <p>
 * You can obtain a user's position by first calling
 * <code>Geolocation.getIfSupported()</code>
 * </p>
 * 
 * <p>
 * Once you have a <code>Geolocation</code>, you can request the user's current
 * position by calling {@link #getCurrentPosition(Callback)} or
 * {@link #watchPosition(Callback)}.
 * </p>
 * 
 * <p>
 * The first time an application requests the user's position, the browser will
 * prompt the user for permission. If the user grants permission, the browser
 * will locate the user and report it back to your application. If the user
 * declines permission, the callback's {@link Callback#onFailure(Object)} method
 * will be called with a {@link PositionError} with its code set to
 * {@link PositionError#PERMISSION_DENIED}.
 * </p>
 * 
 * <p>
 * <span style="color:red;">Experimental API: This API is still under
 * development and is subject to change.</span>
 * 
 * <p>
 * This may not be supported on all browsers.
 * </p>
 * 
 * @see <a href="http://www.w3.org/TR/geolocation-API/">W3C Geolocation API</a>
 * @see <a href="http://diveintohtml5.info/geolocation.html">Dive Into HTML5 -
 *      Geolocation</a>
 */
@PartialSupport
public class Geolocation {

  private static GeolocationSupportDetector detector;
  private static Geolocation impl;

  /**
   * Detector for browser support for Geolocation.
   */
  private static class GeolocationSupportDetector {

    private static native boolean detectSupport() /*-{
      return !!$wnd.navigator.geolocation;
    }-*/;

    private boolean supported = detectSupport();

    public boolean isSupported() {
      return supported;
    }
  }

  /**
   * Additional options for receiving the user's location.
   */
  public static class PositionOptions {
    private boolean enableHighAccuracy = false;
    private int timeout = -1;
    private int maximumAge = 0;

    /**
     * Sets whether or not the application will request a more accurate position
     * from the browser.
     *
     * <p>
     * If the browser supports this option, the user will be prompted to grant
     * permission to this application, even if permission to get the user's
     * (less accurate) position has already been granted.</p>
     *
     * <p>
     * Requesting high accuracy may be slower, or not supported at all,
     * depending on the browser.
     * </p>
     *
     * <p>
     * By default this is <code>false</code>
     * </p>
     */
    public PositionOptions setHighAccuracyEnabled(boolean enabled) {
      this.enableHighAccuracy = enabled;
      return this;
    }

    /**
     * Allows the browser to return a position immediately with a cached
     * position. The maximum age is then the oldest acceptable cached
     * position. If no acceptable cached position is found, the browser will
     * locate the user and cache and return the position.
     *
     * <p>
     * By default this is 0, which means that the position cache will not be
     * used.
     * </p>
     */
    public PositionOptions setMaximumAge(int maximumAge) {
      this.maximumAge = maximumAge;
      return this;
    }

    /**
     * Sets the amount of time (in milliseconds) that the application is willing
     * to wait before getting the user's position. If a request for position
     * takes more than this amount of time, an error will result.
     *
     * <p>
     * By default this is -1, which means there is no application-specified
     * timeout.
     * </p>
     */
    public PositionOptions setTimeout(int timeout) {
      this.timeout = timeout;
      return this;
    }
  }

  /**
   * Returns a {@link Geolocation} if the browser supports this feature, and
   * <code>null</code> otherwise.
   */
  public static Geolocation getIfSupported() {
    if (!isSupported()) {
      return null;
    } else {
      if (impl == null) {
        impl = new Geolocation();
      }
      return impl;
    }
  }

  /**
   * Returns <code>true</code> if the browser supports geolocation.
   */
  public static boolean isSupported() {
    if (detector == null) {
      detector = GWT.create(GeolocationSupportDetector.class);
    }
    return detector.isSupported();
  }

  private static void handleFailure(Callback<Position, PositionError> callback, int code,
      String msg) {
    callback.onFailure(new PositionError(code, msg));
  }

  private static void handleSuccess(Callback<Position, PositionError> callback, PositionImpl pos) {
    callback.onSuccess(pos);
  }

  private static native JavaScriptObject toJso(PositionOptions options) /*-{
    var opt = {};
    if (options) {
      opt.enableHighAccuracy = options.@com.google.gwt.geolocation.client.Geolocation.PositionOptions::enableHighAccuracy;
      opt.maximumAge = options.@com.google.gwt.geolocation.client.Geolocation.PositionOptions::maximumAge;

      if (options.@com.google.gwt.geolocation.client.Geolocation.PositionOptions::timeout > 0) {
        opt.timeout = options.@com.google.gwt.geolocation.client.Geolocation.PositionOptions::timeout;
      }
    }
    return opt;
  }-*/;

  /**
   * Should be instantiated by {@link #getIfSupported()}.
   */
  protected Geolocation() {
  }

  /**
   * Stops watching the user's position.
   *
   * @param watchId the ID of a position watch as returned by a previous call to
   *        {@link #watchPosition(Callback)}.
   */
  public native void clearWatch(int watchId) /*-{
    $wnd.navigator.geolocation.clearWatch(watchId);
  }-*/;

  /**
   * Calls the callback with the user's current position.
   */
  public void getCurrentPosition(Callback<Position, PositionError> callback) {
    getCurrentPosition(callback, null);
  }

  /**
   * Calls the callback with the user's current position, with additional
   * options.
   */
  public native void getCurrentPosition(Callback<Position, PositionError> callback,
      PositionOptions options) /*-{
    var opt = @com.google.gwt.geolocation.client.Geolocation::toJso(*)(options);

    var success = $entry(function(pos) {
      @com.google.gwt.geolocation.client.Geolocation::handleSuccess(*)(callback, pos);
    });

    var failure = $entry(function(err) {
      @com.google.gwt.geolocation.client.Geolocation::handleFailure(*)
      (callback, err.code, err.message);
    });

    if (@com.google.gwt.geolocation.client.Geolocation::isSupported()) {
      $wnd.navigator.geolocation.getCurrentPosition(success, failure, opt);
    }
  }-*/;

  /**
   * Repeatedly calls the given callback with the user's position, as it
   * changes.
   *
   * <p>
   * The frequency of these updates is entirely up to the browser. There is no
   * guarantee that updates will be received at any set interval, but are
   * instead designed to be sent when the user's position changes. This method
   * should be used instead of polling the user's current position.
   * </p>
   *
   * @return the ID of this watch, which can be passed to
   *         {@link #clearWatch(int)} to stop watching the user's position.
   */
  public int watchPosition(Callback<Position, PositionError> callback) {
    return watchPosition(callback, null);
  }

  /**
   * Repeatedly calls the given callback with the user's position, as it
   * changes, with additional options.
   * 
   * <p>
   * The frequency of these updates is entirely up to the browser. There is no
   * guarantee that updates will be received at any set interval, but are
   * instead designed to be sent when the user's position changes. This method
   * should be used instead of polling the user's current position.
   * </p>
   * 
   * <p>
   * If the browser does not support geolocation, this method will do nothing,
   * and will return -1.
   * </p>
   * 
   * @return the ID of this watch, which can be passed to
   *         {@link #clearWatch(int)} to stop watching the user's position.
   */
  public native int watchPosition(Callback<Position, PositionError> callback,
      PositionOptions options) /*-{
    var opt = @com.google.gwt.geolocation.client.Geolocation::toJso(*)(options);

    var success = $entry(function(pos) {
      @com.google.gwt.geolocation.client.Geolocation::handleSuccess(*)(callback, pos);
    });

    var failure = $entry(function(err) {
      @com.google.gwt.geolocation.client.Geolocation::handleFailure(*)
      (callback, err.code, err.message);
    });

    var id = -1;
    if (@com.google.gwt.geolocation.client.Geolocation::isSupported()) {
      id = $wnd.navigator.geolocation.watchPosition(success, failure, opt);
    }
    return id;
  }-*/;
}
