/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 net.java.html.geo;

import java.util.Collections;
import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.java.html.BrwsrCtx;
import org.netbeans.html.context.spi.Contexts;
import org.netbeans.html.geo.impl.Accessor;
import org.netbeans.html.geo.impl.JsGLProvider;
import org.netbeans.html.geo.spi.GLProvider;

/** Class that represents a geolocation position provided as a callback
 * to {@link Handle#onLocation(net.java.html.geo.Position)} method. The
 * class getters mimic closely the structure of the position object as
 * specified by <a href="http://www.w3.org/TR/geolocation-API/">
 * W3C's Geolocation API</a>.
 *
 * @author Jaroslav Tulach
 */
public final class Position {
    static final Logger LOG = Logger.getLogger(Position.class.getName());
    private final long timestamp;
    private final Coordinates coords;

    public Position(long timestamp, Coordinates coords) {
        this.timestamp = timestamp;
        this.coords = coords;
    }
    
    /** The actual location of the position.
     * @return non-null coordinates
     */
    public Coordinates getCoords() {
        return coords;
    }
    
    /** The time when the position has been recorded.
     * @return time in milliseconds since era (e.g. Jan 1, 1970).
     */
    public long getTimestamp() {
        return timestamp;
    }

    /** Actual location of a {@link Position}. 
     *  Mimics closely <a href="http://www.w3.org/TR/geolocation-API/">
     * W3C's Geolocation API</a>.
     */
    public static abstract class Coordinates {
        protected Coordinates() {
            if (!getClass().getName().equals("org.netbeans.html.geo.spi.CoordImpl")) {
                throw new IllegalStateException();
            }
        }
        
        /**
         * @return geographic coordinate specified in decimal degrees.
         */
        public abstract double getLatitude();

        /**
         * @return geographic coordinate specified in decimal degrees.
         */
        public abstract double getLongitude();

        /**
         * The accuracy attribute denotes the accuracy level of the latitude 
         * and longitude coordinates. It is specified in meters. 
         * The value of the accuracy attribute must be a non-negative number.
         * 
         * @return accuracy in meters
         */
        public abstract double getAccuracy();
        
        /** Denotes the height of the position, specified in meters above the ellipsoid. 
         * If the implementation cannot provide altitude information, 
         * the value of this attribute must be null.
         * @return value in meters, may return null, if the information is not available 
         */
        public abstract Double getAltitude();
        
        /**  The altitude accuracy is specified in meters. 
         * If the implementation cannot provide altitude information, 
         * the value of this attribute must be null. Otherwise, the value 
         * must be a non-negative real number.
         * @return value in meters; may return null, if the information is not available 
         */
        public abstract Double getAltitudeAccuracy();
        
        /** Denotes the direction of travel of the device and 
         * is specified in degrees 
         * counting clockwise relative to the true north. 
         * 
         * @return value from 0 to 360 - may return <code>null</code>, 
         *   if the information is not available 
         */
        public abstract Double getHeading();
        
        /** Denotes the magnitude of the horizontal component of the 
         * device's current velocity and is specified in meters per second.
         * 
         * @return may return null, if the information is not available 
         */
        public abstract Double getSpeed();
    } // end of Coordinates

    /** Rather than subclassing this class directly consider using {@link OnLocation}
     * annotation. Such annotation will generate a subclass for you automatically
     * with two static methods <code>createQuery</code> and <code>createWatch</code>
     * which can be used to obtain instance of this class.
     */
    public static abstract class Handle {
        private final boolean oneTime;
        private boolean enableHighAccuracy;
        private long timeout;
        private long maximumAge;
        volatile JsH<?> handle;

        /** Creates new instance of this handle.
         * 
         * @param oneTime <code>true</code> if the handle represents one time 
         *   <em>query</em>. <code>false</code> if it represents a <em>watch</em>
         */
        protected Handle(boolean oneTime) {
            super();
            this.oneTime = oneTime;
        }

        /** Callback from the implementation when a (new) position has been
         * received and identified
         * @param p the position
         * @throws Throwable if an exception is thrown, it will be logged by the system
         */
        protected abstract void onLocation(Position p) throws Throwable;

        /** Callback when an error occurs.
         * @param ex the exception describing what went wrong
         * @throws Throwable if an exception is thrown, it will be logged by the system
         */
        protected abstract void onError(Exception ex) throws Throwable;
        
        /** Check whether the location API is supported.
         * @return true, if one can call {@link #start}.
         */
        public final boolean isSupported() {
            JsH<?> p = seekProviders(null, null);
            if (p != null) {
                p.stop();
                return true;
            }
            return false;
        }

        /** Turns on high accuracy mode as specified by the 
         * <a href="http://www.w3.org/TR/2012/PR­geolocation­API­20120510/">
         * W3C's Geolocation API</a>. By default the mode is disabled.
         * @param enable <code>true</code> or <code>false</code>
         */
        public final void setHighAccuracy(boolean enable) {
            this.enableHighAccuracy = enable;
        }

        /** The amount of milliseconds to wait for a result.
         * By default infinity.
         * @param timeout time in milliseconds to wait for a result.
         */
        public final void setTimeout(long timeout) {
            this.timeout = timeout;
        }

        /** Sets maximum age of cached results which are acceptable to be
         * returned. By default maximum age is set to zero.
         * @param age time in milliseconds of acceptable cached results
         */
        public final void setMaximumAge(long age) {
            this.maximumAge = age;
        }
        
        /** Initializes the <em>query</em> or <em>watch</em> request(s) and
         * returns immediately. Has no effect if the query has already been
         * started. If a problem appears while starting the system,
         * it is immediately reported via the {@link #onError(java.lang.Exception)}
         * callback. For example, if the {@link #isSupported()} method
         * returns <code>false</code> an IllegalStateException is created
         * and sent to the {@link #onError(java.lang.Exception) callback} method.
         */
        public final void start() {
            if (handle != null) {
                return;
            }
            
            Exception[] problem = { null };
            StringBuilder sb = new StringBuilder();
            sb.append("[");
            JsH<?> h = seekProviders(sb, problem);
            sb.append("\n]");
            try {
                if (problem[0] != null) {
                    onError(problem[0]);
                    return;
                }
                if (h == null) {
                    onError(new IllegalStateException("geolocation API not supported. Among providers: " + sb));
                }
                synchronized (this) {
                    if (handle != null) {
                        onError(new IllegalStateException("Parallel request"));
                    }
                    handle = h;
                }
            } catch (Throwable thr) {
                LOG.log(Level.INFO, "Problems delivering onError report", thr);
            }
        }

        private JsH<?> seekProviders(StringBuilder sb, Exception[] problem) {
            BrwsrCtx ctx = BrwsrCtx.findDefault(getClass());
            JsH<?> h = seekProviders(Contexts.find(ctx, GLProvider.class), null, sb, problem);
            if (h == null) {
                h = seekProviders(null, ServiceLoader.load(GLProvider.class), sb, problem);
            }
            if (h == null) {
                h = seekProviders(new JsGLProvider(), null, sb, problem);
            }
            return h;
        }

        private JsH<?> seekProviders(
            GLProvider single, Iterable<GLProvider> set,
            StringBuilder sb, Exception[] problem
        ) {
            if (set == null) {
                if (single == null) {
                    return null;
                }
                set = Collections.singleton(single);
            }
            JsH<?> h = null;
            for (GLProvider<?,?> p : set) {
                if (sb != null) {
                    if (sb.length() > 1) {
                        sb.append(',');
                    }
                    sb.append("\n  ").append(p.getClass().getName());
                }
                try {
                    h = createHandle(p);
                } catch (Exception ex) {
                    LOG.log(Level.INFO, "Problems when starting " + p.getClass().getName(), ex);
                    if (problem != null && problem[0] == null) {
                        problem[0] = ex;
                    }
                }
                if (h != null) {
                    break;
                }
            }
            return h;
        }

        /** Stops all pending requests. After this call no further callbacks
         * can be obtained. Does nothing if no query or watch was in progress.
         */
        public final void stop() {
            JsH h;
            synchronized (this) {
                h = handle;
                if (h == null) {
                    return;
                }
                handle = null;
            }
            h.stop();
        }
        
        private <Watch> JsH<Watch> createHandle(GLProvider<?,Watch> p) {
            JsH<Watch> temp = new JsH<Watch>(p);
            return temp.watch == null ? null : temp;
        }

        private final class JsH<Watch> extends Accessor {
            private final Watch watch;
            private final GLProvider<?, Watch> provider;
            
            public JsH(GLProvider<?, Watch> p) {
                super(true);
                this.watch = Accessor.SPI.start(p, this, oneTime, enableHighAccuracy, timeout, maximumAge);
                this.provider = p;
            }
            
            @Override
            public void onLocation(Position position) {
                if (handle != this) {
                    return;
                }
                if (oneTime) {
                    stop();
                }
                try {
                    Handle.this.onLocation(position);
                } catch (Throwable ex) {
                    LOG.log(Level.SEVERE, null, ex);
                }
            }

            @Override
            public void onError(Exception err) {
                if (handle != this) {
                    return;
                }
                if (oneTime) {
                    stop();
                }
                try {
                    Handle.this.onError(err);
                } catch (Throwable ex) {
                    LOG.log(Level.SEVERE, null, ex);
                }
            }

            protected final void stop() {
                Accessor.SPI.stop(provider, watch);
            }

            @Override
            public <Watch> Watch start(
                GLProvider<?, Watch> p, Accessor peer,
                boolean oneTime, boolean enableHighAccuracy,
                long timeout, long maximumAge
            ) {
                throw new UnsupportedOperationException();
            }

            @Override
            public <Watch> void stop(GLProvider<?, Watch> p, Watch w) {
                throw new UnsupportedOperationException();
            }

        } // end of JsH
    }
}
