package com.github.shadowsocks;
/*
 * Shadowsocks - A shadowsocks client for Android
 * Copyright (C) 2014 <max.c.lv@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 *                            ___====-_  _-====___
 *                      _--^^^#####//      \\#####^^^--_
 *                   _-^##########// (    ) \\##########^-_
 *                  -############//  |\^^/|  \\############-
 *                _/############//   (@::@)   \\############\_
 *               /#############((     \\//     ))#############\
 *              -###############\\    (oo)    //###############-
 *             -#################\\  / VV \  //#################-
 *            -###################\\/      \//###################-
 *           _#/|##########/\######(   /\   )######/\##########|\#_
 *           |/ |#/\#/\#/\/  \#/\##\  |  |  /##/\#/  \/\#/\#/\#| \|
 *           `  |/  V  V  `   V  \#\| |  | |/#/  V   '  V  V  \|  '
 *              `   `  `      `   / | |  | | \   '      '  '   '
 *                               (  | |  | |  )
 *                              __\ | |  | | /__
 *                             (vvv(VVV)(VVV)vvv)
 *
 *                              HERE BE DRAGONS
 *
 */

import android.annotation.SuppressLint;
import android.app.Application;
import android.content.SharedPreferences;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.os.LocaleList;
import android.preference.PreferenceManager;
import android.text.TextUtils;

import com.evernote.android.job.JobManager;
import com.github.shadowsocks.database.DBHelper;
import com.github.shadowsocks.database.Profile;
import com.github.shadowsocks.database.ProfileManager;
import com.github.shadowsocks.database.SSRSubManager;
import com.github.shadowsocks.job.DonaldTrump;
import com.github.shadowsocks.utils.Constants;
import com.github.shadowsocks.utils.IOUtils;
import com.github.shadowsocks.utils.TcpFastOpen;
import com.github.shadowsocks.utils.ToastUtils;
import com.github.shadowsocks.utils.Utils;
import com.github.shadowsocks.utils.VayLog;
import com.google.android.gms.analytics.GoogleAnalytics;
import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.StandardExceptionParser;
import com.google.android.gms.analytics.Tracker;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.tagmanager.Container;
import com.google.android.gms.tagmanager.ContainerHolder;
import com.google.android.gms.tagmanager.TagManager;
import com.j256.ormlite.logger.LocalLog;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
import eu.chainfire.libsuperuser.Shell;

public class ShadowsocksApplication extends Application {
    public static ShadowsocksApplication app;

    private static final String TAG = ShadowsocksApplication.class.getSimpleName();
    public static final String SIG_FUNC = "getSignature";

    private String[] EXECUTABLES = {
            Constants.Executable.PDNSD,
            Constants.Executable.REDSOCKS,
            Constants.Executable.SS_TUNNEL,
            Constants.Executable.SS_LOCAL,
            Constants.Executable.TUN2SOCKS,
            Constants.Executable.KCPTUN};

    /**
     *  The ones in Locale doesn't have script included
     */
    private static final Locale SIMPLIFIED_CHINESE;
    private static final Locale TRADITIONAL_CHINESE;

    static {
        if (Build.VERSION.SDK_INT >= 21) {
            SIMPLIFIED_CHINESE = Locale.forLanguageTag("zh-Hans-CN");
            TRADITIONAL_CHINESE = Locale.forLanguageTag("zh-Hant-TW");
        } else {
            SIMPLIFIED_CHINESE = Locale.SIMPLIFIED_CHINESE;
            TRADITIONAL_CHINESE = Locale.TRADITIONAL_CHINESE;
        }
    }

    public ContainerHolder containerHolder;

    private Tracker tracker;
    public SharedPreferences settings;
    public SharedPreferences.Editor editor;

    public ProfileManager profileManager;
    public SSRSubManager ssrsubManager;
    public Resources resources;

    public boolean isNatEnabled() {
        return settings.getBoolean(Constants.Key.isNAT, false);
    }

    public boolean isVpnEnabled() {
        return !isNatEnabled();
    }

    public ScheduledExecutorService mThreadPool;

    /**
     * /// xhao: init variable
     */
    private void initVariable() {
        tracker = GoogleAnalytics.getInstance(this).newTracker(R.xml.tracker);
        settings = PreferenceManager.getDefaultSharedPreferences(this);
        editor = settings.edit();

        profileManager = new ProfileManager(new DBHelper(this));
        ssrsubManager = new SSRSubManager(new DBHelper(this));
        resources = getResources();

        mThreadPool = new ScheduledThreadPoolExecutor(10, new ThreadFactory() {
            @Override
            public Thread newThread(@NonNull Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("shadowsocks-thread");
                return thread;
            }
        });
    }

    /**
     * send event
     */
    public void track(String category, String action) {
        Map<String, String> builders = new HitBuilders.EventBuilder()
                .setAction(action)
                .setCategory(category)
                .setLabel(BuildConfig.VERSION_NAME)
                .build();
        tracker.send(builders);
    }

    /**
     * send event
     */
    public void track(Throwable t) {
        Map<String, String> builders = new HitBuilders.ExceptionBuilder()
                .setDescription(new StandardExceptionParser(this, null).getDescription(Thread.currentThread().getName(), t))
                .setFatal(false)
                .build();
        tracker.send(builders);
    }

    /**
     * get profile id
     *
     * @return no save return -1
     */
    public int profileId() {
        return settings.getInt(Constants.Key.id, -1);
    }

    /**
     * save profile id
     *
     * @param i profile id
     */
    public void profileId(int i) {
        editor.putInt(Constants.Key.id, i).apply();
    }

    /**
     * current profile
     */
    public Profile currentProfile() {
        return profileManager.getProfile(profileId());
    }

    /**
     * switch profile
     *
     * @param id profile id
     */
    public Profile switchProfile(int id) {
        profileId(id);

        Profile profile = profileManager.getProfile(id);
        if (profile != null) {
            return profile;
        } else {
            return profileManager.createProfile();
        }
    }

    @SuppressLint("NewApi")
    private Locale checkChineseLocale(Locale locale) {
        if ("zh".equals(locale.getLanguage())) {
            String country = locale.getCountry();
            if ("CN".equals(country) || "TW".equals(country)) {
                return null;
            } else {
                String script = locale.getScript();
                if ("Hans".equals(script)) {
                    return SIMPLIFIED_CHINESE;
                } else if ("Hant".equals(script)) {
                    return TRADITIONAL_CHINESE;
                } else {
                    VayLog.w(TAG, String.format("Unknown zh locale script: %s. Falling back to trying countries...", script));
                    if ("SG".equals(country)) {
                        return SIMPLIFIED_CHINESE;
                    } else if ("HK".equals(country) || "MO".equals(country)) {
                        return TRADITIONAL_CHINESE;
                    } else {
                        VayLog.w(TAG, String.format("Unknown zh locale: %s. Falling back to zh-Hans-CN...", locale.toLanguageTag()));
                        return SIMPLIFIED_CHINESE;
                    }
                }
            }
        } else {
            return null;
        }
    }

    /**
     * check chinese locale
     */
    private void checkChineseLocale(Configuration config) {
        if (Build.VERSION.SDK_INT >= 24) {
            LocaleList localeList = config.getLocales();
            Locale[] newList = new Locale[localeList.size()];
            boolean changed = false;
            for (int i = 0; i < localeList.size(); i++) {
                Locale locale = localeList.get(i);
                Locale newLocale = checkChineseLocale(locale);
                if (newLocale == null) {
                    newList[i] = locale;
                } else {
                    newList[i] = newLocale;
                    changed = true;
                }
            }
            if (changed) {
                Configuration newConfig = new Configuration(config);
                newConfig.setLocales(new LocaleList(newList));
                Resources res = getResources();
                res.updateConfiguration(newConfig, res.getDisplayMetrics());
            }
        } else {
            Locale newLocale = checkChineseLocale(config.locale);
            if (newLocale != null) {
                Configuration newConfig = new Configuration(config);
                newConfig.locale = newLocale;
                Resources res = getResources();
                res.updateConfiguration(newConfig, res.getDisplayMetrics());
            }
        }
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        checkChineseLocale(newConfig);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        app = this;
        // init toast utils
        ToastUtils.init(getApplicationContext());
        initVariable();

        if (!BuildConfig.DEBUG) {
            java.lang.System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR");
        }
        AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
        checkChineseLocale(getResources().getConfiguration());
        TagManager tm = TagManager.getInstance(this);
        PendingResult<ContainerHolder> pending = tm.loadContainerPreferNonDefault("GTM-NT8WS8", R.raw.gtm_default_container);
        ResultCallback<ContainerHolder> callback = new ResultCallback<ContainerHolder>() {
            @Override
            public void onResult(@NonNull ContainerHolder holder) {
                if (!holder.getStatus().isSuccess()) {
                    return;
                }
                containerHolder = holder;
                Container container = holder.getContainer();
                container.registerFunctionCallMacroCallback(SIG_FUNC, new Container.FunctionCallMacroCallback() {
                    @Override
                    public Object getValue(String functionName, Map<String, Object> parameters) {
                        if (SIG_FUNC.equals(functionName)) {
                            return Utils.getSignature(getApplicationContext());
                        } else {
                            return null;
                        }
                    }
                });
            }
        };
        pending.setResultCallback(callback, 2, TimeUnit.SECONDS);
        JobManager.create(this).addJobCreator(new DonaldTrump());

        if (settings.getBoolean(Constants.Key.tfo, false) && TcpFastOpen.supported()) {
            mThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    TcpFastOpen.enabled(settings.getBoolean(Constants.Key.tfo, false));
                }
            });
        }
    }

    /**
     * refresh container holder
     */
    public void refreshContainerHolder() {
        if (containerHolder != null) {
            containerHolder.refresh();
        }
    }

    /**
     * copy assets
     *
     * @param path assets path
     */
    private void copyAssets(String path) {
        AssetManager assetManager = getAssets();
        String[] files = null;
        try {
            files = assetManager.list(path);
        } catch (Exception e) {
            VayLog.e(TAG, e.getMessage());
            app.track(e);
        }

        if (files != null) {
            for (String file : files) {
                InputStream in = null;
                FileOutputStream fos = null;
                try {
                    if (!TextUtils.isEmpty(path)) {
                        in = assetManager.open(path + File.separator + file);
                    } else {
                        in = assetManager.open(file);
                    }
                    fos = new FileOutputStream(getApplicationInfo().dataDir + '/' + file);
                    IOUtils.copy(in, fos);
                } catch (IOException e) {
                    VayLog.e(TAG, "copyAssets", e);
                } finally {
                    try {
                        if (in != null) {
                            in.close();
                        }
                    } catch (Exception e) {
                        VayLog.e(TAG, "copyAssets", e);
                    }

                    try {
                        if (fos != null) {
                            fos.close();
                        }
                    } catch (Exception e) {
                        VayLog.e(TAG, "copyAssets", e);
                    }
                }
            }
        }
    }

    /**
     * arash recovery
     */
    public void crashRecovery() {
        ArrayList<String> cmd = new ArrayList<>();

        String[] paramsArray = {"ss-local", "ss-tunnel", "pdnsd", "redsocks", "tun2socks", "proxychains"};
        for (String task : paramsArray) {
            cmd.add(String.format(Locale.ENGLISH, "killall %s", task));
            cmd.add(String.format(Locale.ENGLISH, "rm -f %1$s/%2$s-nat.conf %1$s/%2$s-vpn.conf", getApplicationInfo().dataDir, task));
        }

        // convert to cmd array
        String[] cmds = convertListToStringArray(cmd);
        if (app.isNatEnabled()) {
            cmd.add("iptables -t nat -F OUTPUT");
            cmd.add("echo done");
            List<String> result = Shell.SU.run(cmds);
            if (result != null && !result.isEmpty()) {
                // fallback to SH
                return;
            }
        }

        Shell.SH.run(cmds);
    }

    /**
     * convert list to string array
     *
     * @param list list
     * @return convert failed return {}
     */
    private String[] convertListToStringArray(List<String> list) {
        if (list == null || list.isEmpty()) {
            return new String[]{};
        }

        // start convert
        String[] result = new String[list.size()];
        for (int i = 0; i < list.size(); i++) {
            result[i] = list.get(i);
        }
        return result;
    }

    /**
     * copy assets
     */
    public void copyAssets() {
        // ensure executables are killed before writing to them
        crashRecovery();
        copyAssets(System.getABI());
        copyAssets("acl");

        // exec cmds
        String[] cmds = new String[EXECUTABLES.length];
        for (int i = 0; i < cmds.length; i++) {
            cmds[i] = "chmod 755 " + getApplicationInfo().dataDir + File.separator + EXECUTABLES[i];
        }
        Shell.SH.run(cmds);

        // save current version code
        editor.putInt(Constants.Key.currentVersionCode, BuildConfig.VERSION_CODE).apply();
    }

    /**
     * update assets
     */
    public void updateAssets() {
        if (settings.getInt(Constants.Key.currentVersionCode, -1) != BuildConfig.VERSION_CODE) {
            copyAssets();
        }
    }
}
