/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * 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.android.server.location;

import android.content.Context;
import android.location.Country;
import android.location.CountryListener;
import android.location.Geocoder;
import android.provider.Settings;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Slog;

import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;

/**
 * This class is used to detect the country where the user is. The sources of
 * country are queried in order of reliability, like
 * <ul>
 * <li>Mobile network</li>
 * <li>Location</li>
 * <li>SIM's country</li>
 * <li>Phone's locale</li>
 * </ul>
 * <p>
 * Call the {@link #detectCountry()} to get the available country immediately.
 * <p>
 * To be notified of the future country change, using the
 * {@link #setCountryListener(CountryListener)}
 * <p>
 * Using the {@link #stop()} to stop listening to the country change.
 * <p>
 * The country information will be refreshed every
 * {@link #LOCATION_REFRESH_INTERVAL} once the location based country is used.
 *
 * @hide
 */
public class ComprehensiveCountryDetector extends CountryDetectorBase {

    private final static String TAG = "ComprehensiveCountryDetector";

    /**
     * The refresh interval when the location based country was used
     */
    private final static long LOCATION_REFRESH_INTERVAL = 1000 * 60 * 60 * 24; // 1 day

    protected CountryDetectorBase mLocationBasedCountryDetector;
    protected Timer mLocationRefreshTimer;

    private final int mPhoneType;
    private Country mCountry;
    private TelephonyManager mTelephonyManager;
    private Country mCountryFromLocation;
    private boolean mStopped = false;
    private ServiceState mLastState;

    private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
        @Override
        public void onServiceStateChanged(ServiceState serviceState) {
            // TODO: Find out how often we will be notified, if this method is called too
            // many times, let's consider querying the network.
            Slog.d(TAG, "onServiceStateChanged");
            // We only care the state change
            if (mLastState == null || mLastState.getState() != serviceState.getState()) {
                detectCountry(true, true);
                mLastState = new ServiceState(serviceState);
            }
        }
    };

    /**
     * The listener for receiving the notification from LocationBasedCountryDetector.
     */
    private CountryListener mLocationBasedCountryDetectionListener = new CountryListener() {
        public void onCountryDetected(Country country) {
            mCountryFromLocation = country;
            // Don't start the LocationBasedCountryDetector.
            detectCountry(true, false);
            stopLocationBasedDetector();
        }
    };

    public ComprehensiveCountryDetector(Context context) {
        super(context);
        mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
        mPhoneType = mTelephonyManager.getPhoneType();
    }

    @Override
    public Country detectCountry() {
        // Don't start the LocationBasedCountryDetector if we have been stopped.
        return detectCountry(false, !mStopped);
    }

    @Override
    public void stop() {
        Slog.i(TAG, "Stop the detector.");
        cancelLocationRefresh();
        removePhoneStateListener();
        stopLocationBasedDetector();
        mListener = null;
        mStopped = true;
    }

    /**
     * Get the country from different sources in order of the reliability.
     */
    private Country getCountry() {
        Country result = null;
        result = getNetworkBasedCountry();
        if (result == null) {
            result = getLastKnownLocationBasedCountry();
        }
        if (result == null) {
            result = getSimBasedCountry();
        }
        if (result == null) {
            result = getLocaleCountry();
        }
        return result;
    }

    /**
     * @return the country from the mobile network.
     */
    protected Country getNetworkBasedCountry() {
        String countryIso = null;
        // TODO: The document says the result may be unreliable on CDMA networks. Shall we use
        // it on CDMA phone? We may test the Android primarily used countries.
        if (mPhoneType == TelephonyManager.PHONE_TYPE_GSM) {
            countryIso = mTelephonyManager.getNetworkCountryIso();
            if (!TextUtils.isEmpty(countryIso)) {
                return new Country(countryIso, Country.COUNTRY_SOURCE_NETWORK);
            }
        }
        return null;
    }

    /**
     * @return the cached location based country.
     */
    protected Country getLastKnownLocationBasedCountry() {
        return mCountryFromLocation;
    }

    /**
     * @return the country from SIM card
     */
    protected Country getSimBasedCountry() {
        String countryIso = null;
        countryIso = mTelephonyManager.getSimCountryIso();
        if (!TextUtils.isEmpty(countryIso)) {
            return new Country(countryIso, Country.COUNTRY_SOURCE_SIM);
        }
        return null;
    }

    /**
     * @return the country from the system's locale.
     */
    protected Country getLocaleCountry() {
        Locale defaultLocale = Locale.getDefault();
        if (defaultLocale != null) {
            return new Country(defaultLocale.getCountry(), Country.COUNTRY_SOURCE_LOCALE);
        } else {
            return null;
        }
    }

    /**
     * @param notifyChange indicates whether the listener should be notified the change of the
     * country
     * @param startLocationBasedDetection indicates whether the LocationBasedCountryDetector could
     * be started if the current country source is less reliable than the location.
     * @return the current available UserCountry
     */
    private Country detectCountry(boolean notifyChange, boolean startLocationBasedDetection) {
        Country country = getCountry();
        runAfterDetectionAsync(mCountry != null ? new Country(mCountry) : mCountry, country,
                notifyChange, startLocationBasedDetection);
        mCountry = country;
        return mCountry;
    }

    /**
     * Run the tasks in the service's thread.
     */
    protected void runAfterDetectionAsync(final Country country, final Country detectedCountry,
            final boolean notifyChange, final boolean startLocationBasedDetection) {
        mHandler.post(new Runnable() {
            public void run() {
                runAfterDetection(
                        country, detectedCountry, notifyChange, startLocationBasedDetection);
            }
        });
    }

    @Override
    public void setCountryListener(CountryListener listener) {
        CountryListener prevListener = mListener;
        mListener = listener;
        if (mListener == null) {
            // Stop listening all services
            removePhoneStateListener();
            stopLocationBasedDetector();
            cancelLocationRefresh();
        } else if (prevListener == null) {
            addPhoneStateListener();
            detectCountry(false, true);
        }
    }

    void runAfterDetection(final Country country, final Country detectedCountry,
            final boolean notifyChange, final boolean startLocationBasedDetection) {
        if (notifyChange) {
            notifyIfCountryChanged(country, detectedCountry);
        }
        if (startLocationBasedDetection && (detectedCountry == null
                || detectedCountry.getSource() > Country.COUNTRY_SOURCE_LOCATION)
                && isAirplaneModeOff() && mListener != null && isGeoCoderImplemented()) {
            // Start finding location when the source is less reliable than the
            // location and the airplane mode is off (as geocoder will not
            // work).
            // TODO : Shall we give up starting the detector within a
            // period of time?
            startLocationBasedDetector(mLocationBasedCountryDetectionListener);
        }
        if (detectedCountry == null
                || detectedCountry.getSource() >= Country.COUNTRY_SOURCE_LOCATION) {
            // Schedule the location refresh if the country source is
            // not more reliable than the location or no country is
            // found.
            // TODO: Listen to the preference change of GPS, Wifi etc,
            // and start detecting the country.
            scheduleLocationRefresh();
        } else {
            // Cancel the location refresh once the current source is
            // more reliable than the location.
            cancelLocationRefresh();
            stopLocationBasedDetector();
        }
    }

    /**
     * Find the country from LocationProvider.
     */
    private synchronized void startLocationBasedDetector(CountryListener listener) {
        if (mLocationBasedCountryDetector != null) {
            return;
        }
        mLocationBasedCountryDetector = createLocationBasedCountryDetector();
        mLocationBasedCountryDetector.setCountryListener(listener);
        mLocationBasedCountryDetector.detectCountry();
    }

    private synchronized void stopLocationBasedDetector() {
        if (mLocationBasedCountryDetector != null) {
            mLocationBasedCountryDetector.stop();
            mLocationBasedCountryDetector = null;
        }
    }

    protected CountryDetectorBase createLocationBasedCountryDetector() {
        return new LocationBasedCountryDetector(mContext);
    }

    protected boolean isAirplaneModeOff() {
        return Settings.System.getInt(
                mContext.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) == 0;
    }

    /**
     * Notify the country change.
     */
    private void notifyIfCountryChanged(final Country country, final Country detectedCountry) {
        if (detectedCountry != null && mListener != null
                && (country == null || !country.equals(detectedCountry))) {
            Slog.d(TAG,
                    "The country was changed from " + country != null ? country.getCountryIso() :
                        country + " to " + detectedCountry.getCountryIso());
            notifyListener(detectedCountry);
        }
    }

    /**
     * Schedule the next location refresh. We will do nothing if the scheduled task exists.
     */
    private synchronized void scheduleLocationRefresh() {
        if (mLocationRefreshTimer != null) return;
        mLocationRefreshTimer = new Timer();
        mLocationRefreshTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                mLocationRefreshTimer = null;
                detectCountry(false, true);
            }
        }, LOCATION_REFRESH_INTERVAL);
    }

    /**
     * Cancel the scheduled refresh task if it exists
     */
    private synchronized void cancelLocationRefresh() {
        if (mLocationRefreshTimer != null) {
            mLocationRefreshTimer.cancel();
            mLocationRefreshTimer = null;
        }
    }

    protected synchronized void addPhoneStateListener() {
        if (mPhoneStateListener == null && mPhoneType == TelephonyManager.PHONE_TYPE_GSM) {
            mLastState = null;
            mPhoneStateListener = new PhoneStateListener() {
                @Override
                public void onServiceStateChanged(ServiceState serviceState) {
                    // TODO: Find out how often we will be notified, if this
                    // method is called too
                    // many times, let's consider querying the network.
                    Slog.d(TAG, "onServiceStateChanged");
                    // We only care the state change
                    if (mLastState == null || mLastState.getState() != serviceState.getState()) {
                        detectCountry(true, true);
                        mLastState = new ServiceState(serviceState);
                    }
                }
            };
            mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
        }
    }

    protected synchronized void removePhoneStateListener() {
        if (mPhoneStateListener != null) {
            mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
            mPhoneStateListener = null;
        }
    }

    protected boolean isGeoCoderImplemented() {
        return Geocoder.isPresent();
    }
}
