package cloud.hmml.mmw;

import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.util.Log;
import android.widget.Toast;
import android.Manifest.permission;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.FusedLocationProviderApi;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;

import java.util.Date;

import static android.Manifest.permission.ACCESS_COARSE_LOCATION;

public class ConfigManager implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener {
    public static interface afterLocationUpdate {
         abstract  void afterUpdate();
     }

    private afterLocationUpdate afterUpdateLocation;

    private static final String PREFS_NAME = "cloud.hmml.mmw.WeatherDispWidget";
    private static final String PREF_PREFIX_KEY = "aww_";
    private static final String PREF_KEY_CREATED_AT = PREF_PREFIX_KEY + "created_at_";
    private static final String PREF_KEY_CONFIG_AT = PREF_PREFIX_KEY + "config_at_";
    private static final String PREF_KEY_UPDATED_AT = PREF_PREFIX_KEY + "updated_at_";
    private static final String PREF_KEY_AUTO_LOCATION = PREF_PREFIX_KEY + "auto_loc_";
    private static final String PREF_KEY_LOCATION_NS = PREF_PREFIX_KEY + "loc_ns_";
    private static final String PREF_KEY_LOCATION_ID = PREF_PREFIX_KEY + "loc_id_";
    private static final String PREF_KEY_LOCATION_LABEL = PREF_PREFIX_KEY + "loc_l_";
    private static final String PREF_KEY_TEMP_UNIT = PREF_PREFIX_KEY + "t_unit_";
    private static final String PREF_KEY_P_TEMP_MIN = PREF_PREFIX_KEY + "t_min_";
    private static final String PREF_KEY_P_TEMP_MAX = PREF_PREFIX_KEY + "t_max_";
    private static final String PREF_KEY_P_LABEL = PREF_PREFIX_KEY + "label_";
    private static final String PREF_KEY_P_WEATHER = PREF_PREFIX_KEY + "w_";
    private static final String PREF_KEY_P_POP = PREF_PREFIX_KEY + "pop_";
    private static final String PREF_KEY_REPORTED_AT = PREF_PREFIX_KEY + "reported_at_";
    private static final String PREF_KEY_THEME = PREF_PREFIX_KEY + "theme_";

    static final int REQUEST_PERMISSION_OK = 200;
    static final int LOCATION_WAIT_MS = 2000;
    static final String LOCATION_NS_JMA = "jma";
    static final String LOCATION_NS_OpenWeatherMap = "owm";
    long location_requested_at = 0;
    long location_returned_at = 0;

    private Context context;
    private Context appContext;
    private int appWidgetId;
    private GoogleApiClient gapi = null;

    ConfigManager(Context context, int appWidgetId) {
        this.setContext(context);
        this.setAppWidgetId(appWidgetId);
    }

    public Context getContext() {
        if (context == null || context instanceof Activity && ((Activity) context).isFinishing())
            return appContext;
        return context;
    }

    private void setContext(Context context) {
        this.context = context;
        this.appContext = context.getApplicationContext();
    }

    public int getAppWidgetId() {
        return appWidgetId;
    }

    void setAppWidgetId(int appWidgetId) {
        this.appWidgetId = appWidgetId;
    }

    static String getStringPref(Context context, String key, int widgetId, String default_ret) {
        return getPrefs(context).getString(key + widgetId, default_ret);
    }

    static Long getLongPref(Context context, String key, int widgetId, long default_ret) {
        return getPrefs(context).getLong(key + widgetId, default_ret);
    }

    static Integer getIntPref(Context context, String key, int widgetId, int default_ret) {
        return getPrefs(context).getInt(key + widgetId, default_ret);
    }

    static boolean getBooleanPref(Context context, String key, int widgetId, boolean default_ret) {
        return getPrefs(context).getBoolean(key + widgetId, default_ret);
    }

    static void saveStringPref(Context context, String key, int widgetId, String value) {
        SharedPreferences.Editor editor = getPrefsEditor(context);
        editor.putString(key + widgetId, value);
        editor.apply();
    }

    static void saveLongPref(Context context, String key, int widgetId, long value) {
        SharedPreferences.Editor editor = getPrefsEditor(context);
        editor.putLong(key + widgetId, value);
        editor.apply();
    }

    static void saveIntPref(Context context, String key, int widgetId, int value) {
        SharedPreferences.Editor editor = getPrefsEditor(context);
        editor.putInt(key + widgetId, value);
        editor.apply();
    }

    static void saveBooleanPref(Context context, String key, int widgetId, boolean value) {
        SharedPreferences.Editor editor = getPrefsEditor(context);
        editor.putBoolean(key + widgetId, value);
        editor.apply();
    }

    static SharedPreferences getPrefs(Context context) {
        return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
    }

    static SharedPreferences.Editor getPrefsEditor(Context context) {
        return getPrefs(context).edit();
    }

    // ----------------------- actual value config methods -------------------------------

    static void saveTheme(Context context, int appWidgetId, String themeIdent) {
        saveStringPref(context, PREF_KEY_THEME, appWidgetId, themeIdent);
    }

    void saveTheme(String themeIdent) {
        saveTheme(this.getContext(), this.appWidgetId, themeIdent);
    }

    static void setTheme(Context context, int appWidgetId, Theme theme) {
        if (theme == null) {
            SharedPreferences.Editor editor = getPrefsEditor(context);
            editor.remove(PREF_KEY_THEME +  appWidgetId);
            return;
        }

        saveStringPref(context, PREF_KEY_THEME, appWidgetId, String.valueOf(theme.getType()) + "/" + String.valueOf(theme.getId()));
    }

    static void setTheme(Context context, int appWidgetId, int type, int id) {
        saveStringPref(context, PREF_KEY_THEME, appWidgetId, type + "/" + id);
    }

    void setTheme(Theme theme) {
        setTheme(this.getContext(), this.appWidgetId, theme);
    }

    void setTheme(int type, int id) {
        setTheme(this.getContext(), this.appWidgetId, type, id);
    }

    static String loadTheme(Context context, int appWidgetId) {
        return getStringPref(context, PREF_KEY_THEME, appWidgetId, null);
    }

    String loadTheme() {
        return loadTheme(this.getContext(), this.appWidgetId);
    }

    static Theme getTheme(Context context, int appWidgetId) {
        String themeId = loadTheme(context, appWidgetId);
        Theme theme = null;
        if (themeId != null) {
            String[] idParts = themeId.split("/", 2);
            try {
                theme = Theme.load(context, Integer.parseInt(idParts[0]), Integer.parseInt(idParts[1]));
            } catch (Theme.LoadFailed e) {
                Log.e("theme", "Failed to load theme ("+themeId+"), failling back to default");
            }
        }
        if (theme == null)
            theme = getDefaultTheme(context);
        return theme;
    }

    Theme getTheme() {
        return getTheme(this.getContext(), this.appWidgetId);
    }

    static Theme getDefaultTheme(Context context) {
        Theme theme = Theme.load(context, Theme.TYPE_MIKU_WEATHER, 1);

        if (theme == null)  {
            Log.e("widget", "Failed to load theme! Falling back to default");
            theme = Theme.load(context, Theme.TYPE_BUILTIN, 2);
        }

        if (theme == null) {
            Toast.makeText(context, "MMW Fatal: Failed to load theme!", Toast.LENGTH_LONG).show(); // TODO: translate
            Log.e("widget", "Failed to load theme, fallback to default!!");
        }
        return theme;
    }

    static long saveCreatedAt(Context context, int appWidgetId) {
        SharedPreferences prefs = getPrefs(context);
        Long createdAt = prefs.getLong(PREF_KEY_CREATED_AT + appWidgetId, -1);
        if (createdAt != -1)
            return createdAt;
        SharedPreferences.Editor editor = prefs.edit();
        long now = new Date().getTime();
        editor.putLong(PREF_KEY_CREATED_AT + appWidgetId, now);
        editor.apply();
        return now;
    }

    long saveCreatedAt() {
        return saveCreatedAt(this.getContext(), this.appWidgetId);
    }

    static long saveConfigAt(Context context, int appWidgetId) {
        long now = new Date().getTime();
        saveLongPref(context, PREF_KEY_CONFIG_AT, appWidgetId, now);
        return now;
    }

    long saveConfigAt() {
        return saveConfigAt(this.getContext(), this.appWidgetId);
    }

    static Date loadConfigAt(Context context, int appWidgetId) {
        long ret = getLongPref(context, PREF_KEY_CONFIG_AT, appWidgetId, -1);
        if (ret < 0)
            return null;
        return new Date(ret);
    }

    Date loadConfigAt() {
        return loadConfigAt(this.getContext(), this.appWidgetId);
    }

    static void saveTempUnit(Context context, int appWigetId, String value) {
        saveStringPref(context, PREF_KEY_TEMP_UNIT, appWigetId, value);
    }

    static long saveUpdatedAt(Context context, int appWidgetId) {
        long now = new Date().getTime();
        saveLongPref(context, PREF_KEY_UPDATED_AT, appWidgetId, now);
        return now;
    }

    long saveUpdatedAt() {
        return saveUpdatedAt(this.getContext(), this.appWidgetId);
    }

    void saveTempUnit(String value) {
        saveTempUnit(this.getContext(), this.appWidgetId, value);
    }

    static void saveAutoLocation(Context context, int appWidgetId, boolean value) {
        saveBooleanPref(context, PREF_KEY_AUTO_LOCATION, appWidgetId, value);
    }

    void saveAutoLocation(boolean value) {
        saveAutoLocation(this.getContext(), this.appWidgetId, value);
    }

    static String loadTempUnit(Context context, int appWidgetId) {
        return getStringPref(context, PREF_KEY_TEMP_UNIT, appWidgetId, "c");
    }

    String loadTempUnit() {
        return loadTempUnit(this.getContext(), this.appWidgetId);
    }

    static boolean isTempUnitF(Context context, int appWidgetId) {
        return loadTempUnit(context, appWidgetId).equals("f");
    }

    static boolean isTempUnitC(Context context, int appWidgetId) {
        return loadTempUnit(context, appWidgetId).equals("c");
    }

    boolean isTempUnitF() {
        return isTempUnitF(this.getContext(), appWidgetId);
    }

    boolean isTempUnitC() {
        return isTempUnitC(this.getContext(), appWidgetId);
    }


    static boolean loadAutoLocation(Context context, int appWidgetId) {
        return getBooleanPref(context, PREF_KEY_AUTO_LOCATION, appWidgetId, true);
    }

    boolean loadAutoLocation() {
        return loadAutoLocation(this.getContext(), this.appWidgetId);
    }

    static String loadLocationLabel(Context context, int appWidgetId) {
        return getStringPref(context, PREF_KEY_LOCATION_LABEL, appWidgetId, context.getResources().getString(R.string.unknown_location));
    }

    String loadLocationLabel() {
        return loadLocationLabel(this.getContext(), this.appWidgetId);
    }

    static String loadLocationId(Context context, int appWidgetId) {
        return getStringPref(context, PREF_KEY_LOCATION_ID, appWidgetId, null);
    }

    String loadLocationId() {
        return loadLocationId(this.getContext(), this.appWidgetId);
    }

    static String loadLocationNS(Context context, int appWidgetId) {
        return getStringPref(context, PREF_KEY_LOCATION_NS, appWidgetId, null);
    }

    String loadLocationNS() {
        return loadLocationNS(this.getContext(), this.appWidgetId);
    }

    static void saveLocation(Context context, int appWidgetId, String ns, String id, String label) {
        SharedPreferences.Editor editor = getPrefsEditor(context);
        editor.putString(PREF_KEY_LOCATION_NS + appWidgetId, ns);
        editor.putString(PREF_KEY_LOCATION_ID + appWidgetId, id);
        editor.putString(PREF_KEY_LOCATION_LABEL + appWidgetId, label);
        editor.apply();
    }

    void saveLocation(String ns, String id, String label) {
        saveLocation(this.getContext(), this.appWidgetId, ns, id, label);
    }

    static Date loadCreatedAt(Context context, int appWidgetId) {
        long ret = getLongPref(context, PREF_KEY_CREATED_AT, appWidgetId, -1);
        if (ret < 0)
            return null;
        return new Date(ret);
    }

    Date loadCreatedAt() {
        return loadCreatedAt(this.getContext(), appWidgetId);
    }

    static Date loadUpdatedAt(Context context, int appWidgetId) {
        long ret = getLongPref(context, PREF_KEY_UPDATED_AT, appWidgetId, -1);
        if (ret < 0)
            return null;
        return new Date(ret);
    }

    Date loadUpdatedAt() {
        return loadUpdatedAt(this.getContext(), appWidgetId);
    }

    static Date loadReportedAt(Context context, int appWidgetId) {
        Long ret = getLongPref(context, PREF_KEY_REPORTED_AT, appWidgetId, -1);
        if (ret < 0)
            return null;
        return new Date(ret);
    }

    Date loadReportedAt() {
        return loadReportedAt(this.getContext(), appWidgetId);
    }

    static void saveReportedAt(Context context, int appWidgetId, Date date) {
        if (date == null)
            return;
        saveLongPref(context, PREF_KEY_REPORTED_AT, appWidgetId, date.getTime());
    }

    void saveReportedAt(Date date) {
        saveReportedAt(this.getContext(), appWidgetId, date);
    }


    void saveWeather(int index, String weatherIdent, String label, Integer temp_min, Integer temp_max, Integer pop) {
        if (index < 1 || index > 2) {
            Log.e("config", "Invalid index '"+index+"' for saveWeather");
            return;
        }
        if (weatherIdent == null || label == null)
            return;

        SharedPreferences.Editor editor = getPrefsEditor(this.getContext());

        editor.putString(PREF_KEY_P_WEATHER + index + "_" + appWidgetId, weatherIdent);
        editor.putString(PREF_KEY_P_LABEL   + index + "_" + appWidgetId, label);

        if (temp_max != null)
            editor.putInt(PREF_KEY_P_TEMP_MAX  + index + "_" + appWidgetId, temp_max);

        if (temp_min != null)
            editor.putInt(PREF_KEY_P_TEMP_MIN  + index + "_" + appWidgetId, temp_min);

        if (pop != null)
            editor.putInt(PREF_KEY_P_POP  + index + "_" + appWidgetId, pop);

        editor.apply();
    }

    String loadWeatherIdent(int index) {
        return getStringPref(this.getContext(), PREF_KEY_P_WEATHER + index + "_", appWidgetId, null);
    }

    String loadWeatherLabel(int index) {
        return getStringPref(this.getContext(), PREF_KEY_P_LABEL + index + "_", appWidgetId, null);
    }

    Integer loadTempMax(int index) {
        int ret = getIntPref(this.getContext(), PREF_KEY_P_TEMP_MAX + index + "_", appWidgetId, -100);
        if (ret == -100)
            return null;
        return ret;
    }

    Integer loadTempMin(int index) {
        int ret = getIntPref(this.getContext(), PREF_KEY_P_TEMP_MIN + index + "_", appWidgetId, -100);
        if (ret == -100)
            return null;
        return ret;
    }

    Integer loadPoP(int index) {
        int ret = getIntPref(this.getContext(), PREF_KEY_P_POP + index + "_", appWidgetId, -1);
        if (ret < 0)
            return null;
        return ret;
    }
    // ----------------------- end: actual value config methods -------------------------------

    static void purge(Context context, int appWidgetId) {
        SharedPreferences.Editor prefs = getPrefsEditor(context);
        for (String key: new String[] {
                PREF_KEY_CONFIG_AT,
                PREF_KEY_CREATED_AT,
                PREF_KEY_AUTO_LOCATION,
                PREF_KEY_LOCATION_NS,
                PREF_KEY_LOCATION_ID,
                PREF_KEY_LOCATION_LABEL,
                PREF_KEY_TEMP_UNIT,
                PREF_KEY_REPORTED_AT,
        }) {
            prefs.remove(key + appWidgetId);
        }
        int index = 1;
        for (index = 1; index <= 2; index++) {
            for (String key : new String[]{
                    PREF_KEY_P_TEMP_MIN,
                    PREF_KEY_P_TEMP_MAX,
                    PREF_KEY_P_LABEL,
                    PREF_KEY_P_WEATHER,
                    PREF_KEY_P_POP,
            }) {
                prefs.remove(key + index + "_" + appWidgetId);
            }
        }
        prefs.apply();
    }

    void purge() {
        purge(this.getContext(), this.appWidgetId);
    }


    static boolean checkPermission(Context context, int appWidgetId) {
        return (new ConfigManager(context, appWidgetId)).checkPermission();
    }


    boolean checkPermission() {
        if (!loadAutoLocation(this.getContext(), appWidgetId)) {
            Log.d("cmgr", "Location is set manually. Permission is not required.");
            return true;
        }

        if (ActivityCompat.checkSelfPermission(this.getContext(), ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
            Log.d("cmgr", "Permission is already granted");
            return true;
        } else {
            if (this.getContext() instanceof Activity)
                requestPermission(this.getContext());
            return false;
        }

    }

    static void requestPermission(Context context) {
        if (ActivityCompat.shouldShowRequestPermissionRationale((Activity) context, ACCESS_COARSE_LOCATION)) {
            Toast.makeText(context, R.string.request_perm_location, Toast.LENGTH_SHORT).show();
        }
        Log.d("cmgr", "Requesting Permission...");
        ActivityCompat.requestPermissions((Activity) context, new String[]{ACCESS_COARSE_LOCATION}, REQUEST_PERMISSION_OK);
    }

    static void updateLocation(Context context, int appWidgetId) {
        (new ConfigManager(context, appWidgetId)).updateLocation();
    }

    void updateLocation() {
        updateLocation(null);
    }

    void updateLocation(afterLocationUpdate callback) {
        if (!checkPermission(this.getContext(), this.appWidgetId)) {
            Log.e("cmgr", "Location update request without permission. Ignore!");
            return;
        }

        this.afterUpdateLocation = callback;
        if (callback != null)
            Log.d("cmgr", "Updateing location with callback!");

        if (this.gapi == null) {
            Log.d("cmgr", "Creating google api client");
            this.gapi = new GoogleApiClient.Builder(this.appContext)
                    .addConnectionCallbacks(this)
                    .addOnConnectionFailedListener(this)
                    .addApi(LocationServices.API)
                    .build();
        }

        if (!gapi.isConnected()) {
            gapi.connect();
        } else {
            requestUpdateLocation();
        }
    }

    private void _storeLocation(Location location) {
        if (location == null) {
            Log.w("cmgr", "Location is null!");
            return;
        }

        Log.d("cmgr", "Current Location: lat=" + location.getLatitude() + ", lng=" + location.getLongitude() + ", ac=" + location.getAccuracy());

        // TODO: support multiple location provider!

        double lat = location.getLatitude();
        double lon = location.getLongitude();

        // check Japan area...
        /* TODO!!
        if ((lat >= 23.5 && lat <= 25.5 && lon >= 122.6 && lon <= 126.6) ||
                (lat >= 24.5 && lat <= 31.0 && lon >= 126.0 && lon <= 145.0) ||
                (lat >= 29.0 && lat <= 33.2 && lon >= 128.0 && lon <= 141.0) ||
                (lat >= 29.0 && lat <= 34.6 && lon >= 129.1 && lon <= 130.0) ||
                (lat >= 30.0 && lat <= 34.0 && lon >= 128.5 && lon <= 141.0) ||

                ) {
        }
        */

        String loc_id = null;

        loc_id = JmaArea.findIdByLocation(lat, lon);

        if (loc_id != null)
            saveLocation(LOCATION_NS_JMA, loc_id, JmaAreaData.getLabelById(loc_id));

        if (loc_id == null) {
            Log.w("cmgr", "Connot find area ID!");
            return;
        }


        if (this.afterUpdateLocation != null) {
            Log.d("cmgr", "run callback for location update");
            afterUpdateLocation.afterUpdate();
        } else {
            Log.d("cmgr", "no callback for location update");
        }
        if (this.getContext().getClass() == WeatherDispWidgetConfigureActivity.class) { // TODO: change callback style
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    ((WeatherDispWidgetConfigureActivity) ConfigManager.this.getContext()).applyConfToScreen();
                }
            });
        }
    }

    private void requestUpdateLocation() {
        Log.d("cmgr", "Request update location");
        LocationRequest lreq = new LocationRequest()
                .setPriority(LocationRequest.PRIORITY_LOW_POWER)
                .setMaxWaitTime(LOCATION_WAIT_MS)
                .setNumUpdates(1);

        if (ActivityCompat.checkSelfPermission(this.getContext(), permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this.getContext(), permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            requestPermission(this.getContext());
            return;
        }

        this.location_requested_at = new Date().getTime();
        LocationServices.FusedLocationApi.requestLocationUpdates(gapi, lreq, this);

        new Thread(new Runnable() {
            @Override
            public void run() {
                ConfigManager conf = ConfigManager.this;
                try {
                    Thread.sleep((long) (LOCATION_WAIT_MS * 1.1));
                } catch (InterruptedException e) {
                    // ignore
                }
                if (conf.location_requested_at < conf.location_returned_at)
                    return;
                Log.w("cmgr", "Location request timed out. Fallback to last known.");
                Location mLastLocation = LocationServices.FusedLocationApi.getLastLocation(gapi);
                conf._storeLocation(mLastLocation);
            }
        }).start();
    }

    public void updateWeather() {
        Log.d("cmgr", "Weather update is erquested. In first, call location update...");
        updateLocation(new afterLocationUpdate() {
            @Override
            public void afterUpdate() {
                Log.d("cmgr", "Updating weather by location callback.");
                updateWeatherOnly();
            }
        });
    }

    public static void updateWeather(Context context, int appWidgetId) {
        (new ConfigManager(context, appWidgetId)).updateWeather();
    }

    public void updateWeatherOnly() {
        String ns = loadLocationNS();

        if (ns != null && ns.equals(LOCATION_NS_JMA)) {
            new ForecastFetcher(this.getContext(), ns, loadLocationId()).save(this);
        }
    }

    public static void updateWeatherOnly(Context context, int appWidgetId) {
        (new ConfigManager(context, appWidgetId)).updateWeather();
    }

    @Override
    public void onConnected(@Nullable Bundle bundle) {
        Log.d("cmgr", "API client is connected.");
        requestUpdateLocation();
    }

    @Override
    public void onConnectionSuspended(int i) {
        Log.d("cmgr", "API client is connection suspended.");
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        Log.e("cmgr", "Failed to connect google api client!");
    }

    @Override
    public void onLocationChanged(Location location) {
        Log.d("cmgr", "Location chagned.");
        this.location_returned_at = new Date().getTime();
        if (this.getContext() == null) {
            Log.e("cmgr", "onLocationChange: Current context is null! Abort to save location.");
            return;
        }
        _storeLocation(location);
    }


    public void onDestroy() {
        this.context = null;
        // Note: keep appContext
    }

}
