/*
 * LooperThread
 *
 * Copyright (c) 2014 Renato Villone
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.villoren.android.kalmanlocationmanager.lib;

import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import com.junjunguo.pocketmaps.activities.Permission;

import static com.villoren.android.kalmanlocationmanager.lib.KalmanLocationManager.KALMAN_PROVIDER;
import static com.villoren.android.kalmanlocationmanager.lib.KalmanLocationManager.UseProvider;

/**
 * Created by Rena on 28/09/2014.
 */
class LooperThread extends Thread {
    
    // Static constant
    private static final int THREAD_PRIORITY = 5;

    private static final double DEG_TO_METER = 111225.0;
    private static final double METER_TO_DEG = 1.0 / DEG_TO_METER;

    private static final double TIME_STEP = 1.0;
    private static final double COORDINATE_NOISE = 4.0 * METER_TO_DEG;
    private static final double ALTITUDE_NOISE = 10.0;

    // Context
    private final Context mContext;
    private final Handler mClientHandler;
    private final LocationManager mLocationManager;

    // Settings
    private final UseProvider mUseProvider;
    private final long mMinTimeFilter;
    private final long mMinTimeGpsProvider;
    private final long mMinTimeNetProvider;
    private final LocationListener mClientLocationListener;
    private final boolean mForwardProviderUpdates;

    // Thread
    private Looper mLooper;
    private Handler mOwnHandler;
    private Location mLastLocation;
    private boolean mPredicted;
    private long mMaxPredictTime = -1;
    private boolean mMaxPredictTimeReached = false;

    /**
     * Three 1-dimension trackers, since the dimensions are independent and can avoid using matrices.
     */
    private Tracker1D mLatitudeTracker, mLongitudeTracker, mAltitudeTracker;

    /**
     *
     * @param context
     * @param useProvider
     * @param minTimeFilter
     * @param minTimeGpsProvider
     * @param minTimeNetProvider
     * @param locationListener
     * @param forwardProviderUpdates
     */
    LooperThread(
            Context context,
            UseProvider useProvider,
            long minTimeFilter,
            long minTimeGpsProvider,
            long minTimeNetProvider,
            LocationListener locationListener,
            boolean forwardProviderUpdates)
    {
        mContext = context;
        mClientHandler = new Handler();
        mLocationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);

        mUseProvider = useProvider;

        mMinTimeFilter = minTimeFilter;
        mMinTimeGpsProvider = minTimeGpsProvider;
        mMinTimeNetProvider = minTimeNetProvider;

        mClientLocationListener = locationListener;
        mForwardProviderUpdates = forwardProviderUpdates;

        start();
    }

    @Override
    public void run() {

        setPriority(THREAD_PRIORITY);

        Looper.prepare();
        mLooper = Looper.myLooper();

        if (mUseProvider == UseProvider.GPS || mUseProvider == UseProvider.GPS_AND_NET) {
            requestLoc(LocationManager.GPS_PROVIDER, mMinTimeGpsProvider);
        }

        if (mUseProvider == UseProvider.NET || mUseProvider == UseProvider.GPS_AND_NET) {
            requestLoc(LocationManager.NETWORK_PROVIDER, mMinTimeNetProvider);
        }

        Looper.loop();
    }
    
    private void requestLoc(String provider, long upTime)
    {
        String sPermission = android.Manifest.permission.ACCESS_FINE_LOCATION;
        if (Permission.checkPermission(sPermission, mContext))
        {
            mLocationManager.requestLocationUpdates(
                    provider, upTime, 0.0f, mOwnLocationListener, mLooper);
        }
    }
    
    /** Set maxPredictTime in millis. Disable with negative value. **/
    public void setMaxPredictTime(long maxPredictTime)
    {
        this.mMaxPredictTime = maxPredictTime;
    }

    public void close() {

        mLocationManager.removeUpdates(mOwnLocationListener);
        mLooper.quit();
    }

    private LocationListener mOwnLocationListener = new LocationListener() {

        @Override
        public void onLocationChanged(final Location location) {

            // Reusable
            final double accuracy = location.getAccuracy();
            double position, noise;

            // Latitude
            position = location.getLatitude();
            noise = accuracy * METER_TO_DEG;

            if (mLatitudeTracker == null) {

                mLatitudeTracker = new Tracker1D(TIME_STEP, COORDINATE_NOISE);
                mLatitudeTracker.setState(position, 0.0, noise);
            }

            if (!mPredicted)
                mLatitudeTracker.predict(0.0);

            mLatitudeTracker.update(position, noise);

            // Longitude
            position = location.getLongitude();
            noise = accuracy * Math.cos(Math.toRadians(location.getLatitude())) * METER_TO_DEG ;

            if (mLongitudeTracker == null) {

                mLongitudeTracker = new Tracker1D(TIME_STEP, COORDINATE_NOISE);
                mLongitudeTracker.setState(position, 0.0, noise);
            }

            if (!mPredicted)
                mLongitudeTracker.predict(0.0);

            mLongitudeTracker.update(position, noise);

            // Altitude
            if (location.hasAltitude()) {

                position = location.getAltitude();
                noise = accuracy;

                if (mAltitudeTracker == null) {

                    mAltitudeTracker = new Tracker1D(TIME_STEP, ALTITUDE_NOISE);
                    mAltitudeTracker.setState(position, 0.0, noise);
                }

                if (!mPredicted)
                    mAltitudeTracker.predict(0.0);

                mAltitudeTracker.update(position, noise);
            }

            // Reset predicted flag
            mPredicted = false;

            // Forward update if requested
            if (mForwardProviderUpdates) {

                mClientHandler.post(new Runnable() {

                    @Override
                    public void run() {

                        mClientLocationListener.onLocationChanged(new Location(location));
                    }
                });
            }

            // Update last location
            if (location.getProvider().equals(LocationManager.GPS_PROVIDER)
                    || mLastLocation == null || mLastLocation.getProvider().equals(LocationManager.NETWORK_PROVIDER)) {
                mMaxPredictTimeReached = false;
                mLastLocation = new Location(location);
            }

            // Enable filter timer if this is our first measurement
            if (mOwnHandler == null) {

                mOwnHandler = new Handler(mLooper, mOwnHandlerCallback);
                mOwnHandler.sendEmptyMessageDelayed(0, mMinTimeFilter);
            }
        }

        @Override
        public void onStatusChanged(String provider, final int status, final Bundle extras) {

            final String finalProvider = provider;

            mClientHandler.post(new Runnable() {

                @Override
                public void run() {

                    mClientLocationListener.onStatusChanged(finalProvider, status, extras);
                }
            });
        }

        @Override
        public void onProviderEnabled(String provider) {

            final String finalProvider = provider;

            mClientHandler.post(new Runnable() {

                @Override
                public void run() {

                    mClientLocationListener.onProviderEnabled(finalProvider);
                }
            });
        }

        @Override
        public void onProviderDisabled(String provider) {

            final String finalProvider = provider;

            mClientHandler.post(new Runnable() {

                @Override
                public void run() {

                    mClientLocationListener.onProviderDisabled(finalProvider);
                }
            });
        }
    };

    private Handler.Callback mOwnHandlerCallback = new Handler.Callback() {

        @Override
        public boolean handleMessage(Message msg) {
            if (mMaxPredictTimeReached)
            {
              // Enqueue next prediction
              mOwnHandler.removeMessages(0);
              mOwnHandler.sendEmptyMessageDelayed(0, mMinTimeFilter);
              return true;
            }

            // Prepare location
            final Location location = new Location(KALMAN_PROVIDER);

            // Latitude
            mLatitudeTracker.predict(0.0);
            location.setLatitude(mLatitudeTracker.getPosition());

            // Longitude
            mLongitudeTracker.predict(0.0);
            location.setLongitude(mLongitudeTracker.getPosition());

            // Altitude
            if (mLastLocation.hasAltitude()) {

                mAltitudeTracker.predict(0.0);
                location.setAltitude(mAltitudeTracker.getPosition());
            }

            // Speed
            if (mLastLocation.hasSpeed())
                location.setSpeed(mLastLocation.getSpeed());

            // Bearing
            if (mLastLocation.hasBearing())
                location.setBearing(mLastLocation.getBearing());

            // Accuracy (always has)
            location.setAccuracy((float) (mLatitudeTracker.getAccuracy() * DEG_TO_METER));

            // Set times
            location.setTime(System.currentTimeMillis());

            if (Build.VERSION.SDK_INT >= 17)
                location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
            
            if (mMaxPredictTime > 0)
            {
                long timeDiff = location.getTime() - mLastLocation.getTime();
                if (timeDiff > mMaxPredictTime) { mMaxPredictTimeReached = true; }
            }

            // Post the update in the client (UI) thread
            mClientHandler.post(new Runnable() {

                @Override
                public void run() {

                    mClientLocationListener.onLocationChanged(location);
                }
            });

            // Enqueue next prediction
            mOwnHandler.removeMessages(0);
            mOwnHandler.sendEmptyMessageDelayed(0, mMinTimeFilter);
            mPredicted = true;

            return true;
        }
    };
}