/*
 * Copyright 2015 Hippo Seven
 *
 * 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.hippo.nimingban;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Debug;
import android.support.annotation.NonNull;
import android.util.Log;

import com.alibaba.fastjson.JSON;
import com.hippo.conaco.Conaco;
import com.hippo.drawable.ImageWrapper;
import com.hippo.nimingban.client.NMBClient;
import com.hippo.nimingban.client.NMBDns;
import com.hippo.nimingban.client.NMBInterceptor;
import com.hippo.nimingban.client.NMBRequest;
import com.hippo.nimingban.client.ac.data.ACCdnPath;
import com.hippo.nimingban.client.ac.data.ACForum;
import com.hippo.nimingban.client.ac.data.ACForumGroup;
import com.hippo.nimingban.client.data.ACSite;
import com.hippo.nimingban.network.HttpCookieDB;
import com.hippo.nimingban.network.HttpCookieWithId;
import com.hippo.nimingban.network.SimpleCookieStore;
import com.hippo.nimingban.ui.BringToForegroundActivity;
import com.hippo.nimingban.util.BitmapUtils;
import com.hippo.nimingban.util.Crash;
import com.hippo.nimingban.util.DB;
import com.hippo.nimingban.util.ForumAutoSortingUtils;
import com.hippo.nimingban.util.ReadableTime;
import com.hippo.nimingban.util.ResImageGetter;
import com.hippo.nimingban.util.Settings;
import com.hippo.nimingban.widget.ImageWrapperHelper;
import com.hippo.okhttp.CookieDBJar;
import com.hippo.util.NetworkUtils;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.Messenger;
import com.hippo.yorozuya.Say;
import com.hippo.yorozuya.SimpleHandler;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.net.HttpCookie;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public final class NMBApplication extends Application
        implements Thread.UncaughtExceptionHandler, Messenger.Receiver, Runnable {

    private static final String TAG = NMBApplication.class.getSimpleName();

    private static final boolean LOG_NATIVE_MEMORY = false;

    private static final String AC_CDN_PATH_FILENAME = "ac_cdn_path";

    private Thread.UncaughtExceptionHandler mDefaultHandler;

    private SimpleCookieStore mSimpleCookieStore;
    private NMBClient mNMBClient;
    private Conaco<ImageWrapper> mConaco;
    private ImageWrapperHelper mImageWrapperHelper;
    private OkHttpClient mOkHttpClient;

    private boolean mConnectedWifi;

    private List<WeakReference<Activity>> mActivities = new ArrayList<>();

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

        // Prepare to crash
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);

        NMBAppConfig.initialize(this);
        File logFile = NMBAppConfig.getFileInAppDir("nimingban.log");
        if (logFile != null) {
            Say.initSayFile(logFile);
        }
        Settings.initialize(this);
        DB.initialize(this);
        HttpCookieDB.initialize(this);
        ReadableTime.initialize(this);
        BitmapUtils.initialize(this);
        ResImageGetter.initialize(this);
        Emoji.initialize(this);

        // Remove temp file
        FileUtils.deleteContent(NMBAppConfig.getTempDir());

        // Check network state
        updateNetworkState(this);
        registerReceiver(new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                updateNetworkState(context);
            }
        }, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));

        // Theme
        setTheme(Settings.getDarkTheme() ? R.style.AppTheme_Dark : R.style.AppTheme);
        Messenger.getInstance().register(Constants.MESSENGER_ID_CHANGE_THEME, this);

        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                mActivities.add(new WeakReference<>(activity));
            }
            @Override
            public void onActivityDestroyed(Activity activity) {
                Iterator<WeakReference<Activity>> iterator = mActivities.iterator();
                while (iterator.hasNext()) {
                    WeakReference<Activity> reference = iterator.next();
                    Activity a = reference.get();
                    // Remove current activity and null
                    if (a == null || a == activity) {
                        iterator.remove();
                    }
                }
            }
            @Override
            public void onActivityStarted(Activity activity) {}
            @Override
            public void onActivityResumed(Activity activity) {}
            @Override
            public void onActivityPaused(Activity activity) {}
            @Override
            public void onActivityStopped(Activity activity) {}
            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
        });

        try {
            update();
        } catch (PackageManager.NameNotFoundException e) {
            // Ignore
        }

        if (LOG_NATIVE_MEMORY) {
            SimpleHandler.getInstance().post(this);
        }

        start();
    }

    private void start() {
        updateACCdnPath();
        updateACForums();
        updateACHost();

        long lastForumAging = Settings.getLastForumAging();
        long time = System.currentTimeMillis();
        if (time - lastForumAging > 24 * 60 * 60 * 1000) { // 24 hr * 60 min * 60 sec * 1000 milli
            ForumAutoSortingUtils.ageACForumFrequency();
            Settings.setLastForumAging(time);
        }
    }

    private void readACCdnPathFromFile() {
        File file = new File(getFilesDir(), AC_CDN_PATH_FILENAME);
        InputStream is = null;
        try {
            is = new FileInputStream(file);
            String str = IOUtils.readString(is, "utf-8");
            List<ACCdnPath> list = JSON.parseArray(str, ACCdnPath.class);
            ACSite.getInstance().setCdnPath(list);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(is);
        }
    }

    private void writeACCdnPathToFile(List<ACCdnPath> cdnPaths) {
        File file = new File(getFilesDir(), AC_CDN_PATH_FILENAME);
        OutputStream os = null;
        try {
            os = new FileOutputStream(file);
            os.write(JSON.toJSONString(cdnPaths).getBytes("utf-8"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(os);
        }
    }

    private void updateACCdnPath() {
        // First read cdn path from file
        readACCdnPathFromFile();

        NMBRequest request = new NMBRequest();
        request.setSite(ACSite.getInstance());
        request.setMethod(NMBClient.METHOD_GET_CDN_PATH);
        request.setCallback(new NMBClient.Callback<List<ACCdnPath>>(){
            @Override
            public void onSuccess(List<ACCdnPath> result) {
                ACSite.getInstance().setCdnPath(result);
                writeACCdnPathToFile(result);
            }

            @Override
            public void onFailure(Exception e) {
                e.printStackTrace();
            }

            @Override
            public void onCancel() {
            }
        });
        getNMBClient(this).execute(request);
    }

    private void updateACForums() {
        NMBRequest request = new NMBRequest();
        request.setSite(ACSite.getInstance());
        request.setMethod(NMBClient.METHOD_GET_FORUM_LIST);
        request.setCallback(new NMBClient.Callback<List<ACForumGroup>>(){
            @Override
            public void onSuccess(List<ACForumGroup> result) {
                List<ACForum> list = new LinkedList<>();
                for (ACForumGroup forumGroup : result) {
                    list.addAll(forumGroup.forums);
                }
                DB.setACForums(list);
            }
            @Override
            public void onFailure(Exception e) { }
            @Override
            public void onCancel() { }
        });
        getNMBClient(this).execute(request);
    }

    @SuppressLint("StaticFieldLeak")
    private void updateACHost() {
        getNMBClient(this).execute(new AsyncTask<Object, Object, String>() {
            @Override
            protected String doInBackground(Object... objects) {
                OkHttpClient client = getOkHttpClient(NMBApplication.this);
                Request request = new Request.Builder().url("http://adnmb.com").build();
                Call call = client.newCall(request);
                try {
                    Response response = call.execute();
                    String url = response.request().url().toString();
                    response.close();

                    // Find third '/', make url like http://adnmb.com
                    int count = 0;
                    for (int i = 0, n = url.length(); i < n; i++) {
                        if (url.charAt(i) == '/') {
                            count++;
                        }
                        if (count == 3) {
                            url = url.substring(0, i);
                            break;
                        }
                    }

                    return url;
                } catch (IOException e) {
                    e.printStackTrace();
                    return null;
                }
            }

            @Override
            protected void onPostExecute(String host) {
                Log.d(TAG, "ac host = " + host);
                Settings.putAcHost(host);
            }
        });
    }

    private void update() throws PackageManager.NameNotFoundException {
        int oldVersionCode = Settings.getVersionCode();
        PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_ACTIVITIES);
        Settings.putVersionCode(pi.versionCode);

        if (oldVersionCode < 6) {
            updateCookies(this);
        }

        if (oldVersionCode < 14) {
            Settings.putGuideListActivity(true);
        }

        if (oldVersionCode < 42) {
            Settings.putSetAnalysis(false);
            Settings.putAnalysis(false);
        }

        // Fix cookie lost when save to file in 1.2.29 and below
        if (oldVersionCode < 44) {
            NMBApplication.getSimpleCookieStore(this).fixLostCookiePath();
        }
    }

    public static void updateCookies(Context context) {
        SimpleCookieStore cookieStore = NMBApplication.getSimpleCookieStore(context);

        URL url = ACSite.getInstance().getSiteUrl();
        HttpCookieWithId hcwi = cookieStore.getCookie(url, "userId");
        if (hcwi != null) {
            HttpCookie oldCookie = hcwi.httpCookie;
            cookieStore.remove(url, oldCookie);

            HttpCookie newCookie = new HttpCookie("userhash", oldCookie.getValue());
            newCookie.setComment(oldCookie.getComment());
            newCookie.setCommentURL(oldCookie.getCommentURL());
            newCookie.setDiscard(oldCookie.getDiscard());
            newCookie.setDomain(oldCookie.getDomain());
            newCookie.setMaxAge(oldCookie.getMaxAge());
            newCookie.setPath(oldCookie.getPath());
            newCookie.setPortlist(oldCookie.getPortlist());
            newCookie.setSecure(oldCookie.getSecure());
            newCookie.setVersion(oldCookie.getVersion());

            cookieStore.add(url, newCookie);
        }
    }

    public void bringActivitiesToForeground() {
        int i = mActivities.size();
        while (--i >= 0) {
            Activity activity = mActivities.get(i).get();
            if (activity != null && !"com.hippo.nimingban.wxapi.WXEntryActivity".equals(activity.getClass().getName())) {
                Intent intent = new Intent(activity, BringToForegroundActivity.class);
                activity.startActivity(intent);
            }
        }
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);

        if (level ==  TRIM_MEMORY_BACKGROUND ) {
            if (mConaco != null) {
                mConaco.clearMemoryCache();
            }
        }
    }

    public static void updateNetworkState(Context context) {
        ((NMBApplication) context.getApplicationContext()).mConnectedWifi =
                NetworkUtils.isConnectedWifi(context);
    }

    public static boolean isConnectedWifi(Context context) {
        return ((NMBApplication) context.getApplicationContext()).mConnectedWifi;
    }

    public static SimpleCookieStore getSimpleCookieStore(@NonNull Context context) {
        NMBApplication application = ((NMBApplication) context.getApplicationContext());
        if (application.mSimpleCookieStore == null) {
            application.mSimpleCookieStore = new SimpleCookieStore();
        }
        return application.mSimpleCookieStore;
    }

    @NonNull
    public static NMBClient getNMBClient(@NonNull Context context) {
        NMBApplication application = ((NMBApplication) context.getApplicationContext());
        if (application.mNMBClient == null) {
            application.mNMBClient = new NMBClient(application);
        }
        return application.mNMBClient;
    }

    private static int getMemoryCacheMaxSize(Context context) {
        final ActivityManager activityManager = (ActivityManager) context.
                getSystemService(Context.ACTIVITY_SERVICE);
        return Math.min(20 * 1024 * 1024,
                Math.round(0.2f * activityManager.getMemoryClass() * 1024 * 1024));
    }

    @NonNull
    public static Conaco<ImageWrapper> getConaco(@NonNull Context context) {
        NMBApplication application = ((NMBApplication) context.getApplicationContext());
        if (application.mConaco == null) {
            Conaco.Builder<ImageWrapper> builder = new Conaco.Builder<>();
            builder.hasMemoryCache = true;
            builder.memoryCacheMaxSize = getMemoryCacheMaxSize(context);
            builder.hasDiskCache = true;
            builder.diskCacheDir = new File(context.getCacheDir(), "thumb");
            builder.diskCacheMaxSize = 80 * 1024 * 1024; // 80MB
            builder.okHttpClient = getOkHttpClient(context);
            builder.objectHelper = getImageWrapperHelper(context);
            application.mConaco = builder.build();
        }
        return application.mConaco;
    }

    @NonNull
    public static ImageWrapperHelper getImageWrapperHelper(@NonNull Context context) {
        NMBApplication application = ((NMBApplication) context.getApplicationContext());
        if (application.mImageWrapperHelper == null) {
            application.mImageWrapperHelper = new ImageWrapperHelper();
        }
        return application.mImageWrapperHelper;
    }

    public static OkHttpClient getOkHttpClient(@NonNull Context context) {
        NMBApplication application = ((NMBApplication) context.getApplicationContext());
        if (application.mOkHttpClient == null) {
            application.mOkHttpClient = new OkHttpClient.Builder()
                    .connectTimeout(15, TimeUnit.SECONDS)
                    .readTimeout(15, TimeUnit.SECONDS)
                    .writeTimeout(15, TimeUnit.SECONDS)
                    .dns(new NMBDns())
                    .cookieJar(new CookieDBJar(getSimpleCookieStore(context)))
                    .addInterceptor(new NMBInterceptor())
                    .build();
        }
        return application.mOkHttpClient;
    }

    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        if (!handleException(ex) && mDefaultHandler != null) {
            mDefaultHandler.uncaughtException(thread, ex);
        }
        android.os.Process.killProcess(android.os.Process.myPid());
        System.exit(1);
    }

    private boolean handleException(Throwable ex) {
        if (ex == null) {
            return false;
        }
        try {
            ex.printStackTrace();
            Crash.saveCrashInfo2File(this, ex);
            return true;
        } catch (Throwable tr) {
            return false;
        }
    }

    @Override
    public void onReceive(int id, Object obj) {
        setTheme((Boolean) obj ? R.style.AppTheme_Dark : R.style.AppTheme);
    }

    @Override
    public void run() {
        Log.i(TAG, "Native " + FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false));
        SimpleHandler.getInstance().postDelayed(this, 3000);
    }
}