/**
 * Copyright (C) 2013 by Raphael Michel under the MIT license:
 *
 * 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 de.geeksfactory.opacclient;

import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;

import org.joda.time.DateTime;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.multidex.MultiDex;
import androidx.preference.PreferenceManager;
import de.geeksfactory.opacclient.apis.OpacApi;
import de.geeksfactory.opacclient.frontend.AccountListActivity;
import de.geeksfactory.opacclient.frontend.LibraryListActivity;
import de.geeksfactory.opacclient.frontend.MainActivity;
import de.geeksfactory.opacclient.frontend.MainPreferenceActivity;
import de.geeksfactory.opacclient.frontend.SearchResultListActivity;
import de.geeksfactory.opacclient.i18n.AndroidStringProvider;
import de.geeksfactory.opacclient.networking.AndroidHttpClientFactory;
import de.geeksfactory.opacclient.objects.Account;
import de.geeksfactory.opacclient.objects.Library;
import de.geeksfactory.opacclient.reminder.SyncAccountJob;
import de.geeksfactory.opacclient.searchfields.SearchField;
import de.geeksfactory.opacclient.searchfields.SearchQuery;
import de.geeksfactory.opacclient.storage.AccountDataSource;
import de.geeksfactory.opacclient.storage.PreferenceDataSource;
import de.geeksfactory.opacclient.storage.StarContentProvider;
import de.geeksfactory.opacclient.utils.DebugTools;
import de.geeksfactory.opacclient.utils.ErrorReporter;
import de.geeksfactory.opacclient.utils.GooglePlayTools;
import de.geeksfactory.opacclient.utils.Utils;
import de.geeksfactory.opacclient.webservice.LibraryConfigUpdateService;
import de.geeksfactory.opacclient.webservice.UpdateHandler;
import de.geeksfactory.opacclient.webservice.WebserviceReportHandler;
import io.sentry.android.core.SentryAndroid;
import io.sentry.core.Sentry;

public class OpacClient extends Application {

    public static final String PREF_SELECTED_ACCOUNT = "selectedAccount";
    public static final String PREF_HOME_BRANCH_PREFIX = "homeBranch_";
    public static final String ASSETS_BIBSDIR = "bibs";
    public static final String SENTRY_LIBRARY = "library";
    public static final String SENTRY_DATA_VERSION = "data_version";
    public static final String SENTRY_PACKAGE = "package";
    public static int NOTIF_ID = 1;
    public static int BROADCAST_REMINDER = 2;
    public static Context context;
    public static String versionName = "unknown";
    private static OpacClient instance;
    public final boolean SLIDING_MENU = true;
    private final Uri STAR_PROVIDER_STAR_URI = StarContentProvider.STAR_URI;
    protected Account account;
    protected OpacApi api;
    protected Library library;
    protected String currentLang;
    private SharedPreferences sp;

    public OpacClient() {
        super();
        instance = this;
    }

    public static Context getEmergencyContext() {
        return instance.getApplicationContext();
    }

    public static Bundle queryToBundle(List<SearchQuery> query) {
        if (query == null) {
            return null;
        }
        Bundle b = new Bundle();
        for (SearchQuery q : query) {
            try {
                b.putString(q.getSearchField().toJSON().toString(),
                        q.getValue());
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
        return b;
    }

    // public Class getSearchResultsActivityClass() {
    // return SearchResultsActivity.class;
    // }

    public static List<SearchQuery> bundleToQuery(Bundle bundle) {
        if (bundle == null) {
            return null;
        }
        List<SearchQuery> query = new ArrayList<>();
        for (String e : bundle.keySet()) {
            try {
                query.add(new SearchQuery(SearchField
                        .fromJSON(new JSONObject(e)), bundle.getString(e)));
            } catch (JSONException e1) {
                e1.printStackTrace();
            }
        }
        return query;
    }

    public static Bundle mapToBundle(Map<String, String> map) {
        if (map == null) {
            return null;
        }
        Bundle b = new Bundle();
        for (Entry<String, String> e : map.entrySet()) {
            b.putString(e.getKey(), e.getValue());
        }
        return b;
    }

    public static Map<String, String> bundleToMap(Bundle bundle) {
        if (bundle == null) {
            return null;
        }
        Map<String, String> map = new HashMap<>();
        for (String e : bundle.keySet()) {
            map.put(e, (String) bundle.get(e));
        }
        return map;
    }

    public Uri getStarProviderStarUri() {
        return STAR_PROVIDER_STAR_URI;
    }

    public void addFirstAccount(Activity activity) {
        Intent intent = new Intent(activity, LibraryListActivity.class);
        intent.putExtra(LibraryListActivity.EXTRA_WELCOME, true);
        activity.startActivity(intent);
        activity.finish();
    }

    public void startSearch(Activity caller, List<SearchQuery> query) {
        startSearch(caller, query, null);
    }

    public void startSearch(Activity caller, List<SearchQuery> query, Bundle bundle) {
        Intent myIntent = new Intent(caller, SearchResultListActivity.class);
        myIntent.putExtra("query", queryToBundle(query));
        ActivityCompat.startActivity(caller, myIntent, bundle);
    }

    public void startVolumeSearch(Activity caller, Map<String, String> query) {
        Intent myIntent = new Intent(caller, SearchResultListActivity.class);
        myIntent.putExtra("volumeQuery", mapToBundle(query));
        caller.startActivity(myIntent);
    }

    public boolean isOnline() {
        ConnectivityManager connMgr =
                (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
        return (networkInfo != null && networkInfo.isConnected());
    }

    public synchronized OpacApi getNewApi(Library lib) throws LibraryRemovedException {
        if (!lib.isActive()) throw new LibraryRemovedException();
        currentLang = getResources().getConfiguration().locale.getLanguage();
        return OpacApiFactory
                .create(lib, new AndroidStringProvider(), new AndroidHttpClientFactory(),
                        currentLang, new WebserviceReportHandler(getApplicationContext()),
                        BuildConfig.DEBUG, CoverDownloadStrategy.ASYNCHRONOUS);
    }

    private OpacApi initApi(Library lib) throws LibraryRemovedException {
        api = getNewApi(lib);
        return api;
    }

    public void resetCache() {
        account = null;
        api = null;
        library = null;
    }

    public OpacApi getApi() throws LibraryRemovedException {
        if (account != null && api != null) {
            if (sp.getLong(PREF_SELECTED_ACCOUNT, 0) == account.getId()
                    && getResources().getConfiguration().locale.getLanguage()
                                                               .equals(currentLang)) {
                return api;
            }
        }
        api = initApi(getLibrary());
        return api;
    }

    public long createAccountForLibrary(Context activity, String libraryIdent) {
        AccountDataSource data = new AccountDataSource(activity);

        Account acc = new Account();
        acc.setLibrary(libraryIdent);
        acc.setLabel(getString(R.string.default_account_name));

        return data.addAccount(acc);
    }

    public Account getAccount() {
        if (account != null) {
            if (sp.getLong(PREF_SELECTED_ACCOUNT, 0) == account.getId()) {
                return account;
            }
        }
        AccountDataSource data = new AccountDataSource(this);
        account = data.getAccount(sp.getLong(PREF_SELECTED_ACCOUNT, 0));
        return account;
    }

    public Account getAccountByLibrary(String libraryIdent) {
        AccountDataSource data = new AccountDataSource(this);
        List<Account> accounts = data.getAllAccounts(libraryIdent);

        if(accounts.size() > 0) {
            return accounts.get(0);
        }

        return null;
    }

    public void setAccount(long id) {
        sp.edit().putLong(OpacClient.PREF_SELECTED_ACCOUNT, id).apply();
        resetCache();
        if (getLibrary() != null && !BuildConfig.DEBUG) {
            Sentry.setTag(SENTRY_LIBRARY, getLibrary().getIdent());
        }
    }

    public Library getLibrary(String ident) throws IOException, JSONException {
        String filename = ident + ".json";
        String json;

        File filesDir = getLibrariesDir();
        PreferenceDataSource prefs = getPreferenceDataSource();

        if (fileExists(filesDir, filename) && (
                !prefs.hasBundledConfiguration() ||
                        prefs.getLastLibraryConfigUpdateVersion() == BuildConfig.VERSION_CODE)) {
            // only use files if they were downloaded using the current app version
            json = Utils.readStreamToString(openFile(filesDir, filename));
        } else {
            json = Utils.readStreamToString(getAssets().open(ASSETS_BIBSDIR + "/" + filename));
        }
        return Library.fromJSON(ident, new JSONObject(json));
    }

    @NonNull
    PreferenceDataSource getPreferenceDataSource() {
        return new PreferenceDataSource(this);
    }

    InputStream openFile(File filesDir, String filename) throws FileNotFoundException {
        File file = new File(filesDir, filename);
        return new FileInputStream(file);
    }

    boolean fileExists(File filesDir, String filename) {
        File file = new File(filesDir, filename);
        return file.exists();
    }

    @NonNull
    protected File getFile(File filesDir, String filename) {
        return new File(filesDir, filename);
    }

    @NonNull
    protected File getLibrariesDir() {
        File filesDir = new File(getFilesDir(), LibraryConfigUpdateService.LIBRARIES_DIR);
        filesDir.mkdirs();
        return filesDir;
    }

    public Library getLibrary() {
        if (getAccount() == null) {
            return null;
        }
        if (account != null && library != null) {
            if (sp.getLong(PREF_SELECTED_ACCOUNT, 0) == account.getId()) {
                return library;
            }
        }
        try {
            library = getLibrary(getAccount().getLibrary());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (JSONException e) {
            ErrorReporter.handleException(e);
        }
        return library;
    }

    @SuppressWarnings("UnusedDeclaration") // ProgressCallback should not be required
    public List<Library> getLibraries() throws IOException {
        return getLibraries(null);
    }

    public List<Library> getLibraries(ProgressCallback callback)
            throws IOException {
        AssetManager assets = getAssets();
        String[] files = assets.list(ASSETS_BIBSDIR);
        String[] additionalFiles = getLibrariesDir().list();
        Set<String> allFiles = new HashSet<>();
        Collections.addAll(allFiles, files);
        Collections.addAll(allFiles, additionalFiles);
        int num = allFiles.size();

        List<Library> libs = new ArrayList<>();

        int i = 0;
        for (String file : allFiles) {
            try {
                Library lib =
                        getLibrary(file.substring(0, file.length() - ".json".length()));
                if ((!lib.getApi().equals("test") || BuildConfig.DEBUG) && lib.isActive()) {
                    libs.add(lib);
                }
            } catch (JSONException e) {
                Log.w("JSON library files", "Failed parsing library " + file);
                e.printStackTrace();
            }
            if (callback != null && i % 10 == 0 && i > 0) {
                // reporting progress for every 10 loaded files should be enough
                callback.publishProgress(((double) i) / num);
            }
            i++;
        }

        return libs;
    }

    public void toPrefs(Activity activity) {
        Intent intent = new Intent(activity, MainPreferenceActivity.class);
        activity.startActivity(intent);
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }

    @Override
    public void onCreate() {
        super.onCreate();

        GooglePlayTools.updateSecurityProvider(getApplicationContext());

        sp = PreferenceManager.getDefaultSharedPreferences(this);

        if (BuildConfig.SENTRY_DSN != null) {
            SentryAndroid.init(this, options -> {
                options.setDsn(BuildConfig.SENTRY_DSN);
            });
            if (getLibrary() != null) {
                Sentry.setTag(SENTRY_LIBRARY, getLibrary().getIdent());
            }
            DateTime lastUpdate = new PreferenceDataSource(getApplicationContext())
                    .getLastLibraryConfigUpdate();
            Sentry.setExtra(
                    SENTRY_DATA_VERSION, lastUpdate != null ? lastUpdate.toString() : "null");
            Sentry.setExtra(
                    SENTRY_PACKAGE, getPackageName()
            );
        }

        DebugTools.init(this);

        OpacClient.context = getApplicationContext();

        try {
            OpacClient.versionName = getPackageManager().getPackageInfo(
                    getPackageName(), 0).versionName;
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        }

        // Schedule alarms
        SyncAccountJob.scheduleJob(this);
    }

    public boolean getSlidingMenuEnabled() {
        return SLIDING_MENU;
    }

    public Class<?> getMainActivity() {
        return MainActivity.class;
    }

    public void openAccountList(Activity ctx) {
        Intent intent = new Intent(ctx, AccountListActivity.class);
        ctx.startActivity(intent);
    }

    public interface ProgressCallback {
        public void publishProgress(double progress);
    }

    public static class LibraryRemovedException extends Exception {
    }

    public UpdateHandler getUpdateHandler() {
        return new UpdateHandler();
    }

    public boolean promotePlusApps() {
        return true;
    }

    public OpacApi getApiFromMetaSearch(String libraryIdent) throws LibraryRemovedException {
        // in open source version there is no meta search
        // we always only use the current selected library
        return getApi();
    }
}
