/*
 * Copyright (c) 2016 Route1 Inc.  All rights reerved.  <sales@route1.com>
 * http://www.route1.com
 *
 * You may use, distribute and modify this code under the
 * terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.
 *
 * This code is provided “as -is,” without warranty of any kind,
 * express, implied or statutory, including but not limited to
 * any implied warranties or conditions of quality, performance,
 * merchantability, merchantable quality, durability,
 * non-infringement or of fitness for a particular purpose.
 * See the GPLv2 for more details.
 *
 * Contributor:  A. M. Jehad Sarkar  <amjehad.sarkar@route1.com>
*/


package com.routeone.dc.vpn;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.os.Build;
import android.os.Handler;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import de.blinkt.openvpn.R;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ICSOpenVPNApplication;

public class DcUtils
{
    private static final String TAG = "DcUtils";
    public static final  int    DC_REQUEST_TIMEOUT  = 60000;
    public static final  int    VERIFY_PIN_TIMEOUT  = 90000;
    private static final String TRUST_FILE_NAME = "trust.pem";
    private static final String TA_KEY_FILE_NAME = "ta.key";
    private static ProgressDialog verifyDialog = null;
    private static AlertDialog errorDialog = null;
    private static Handler timerHandler = null;

    public static boolean isPackageInstalled(String packagename, PackageManager packageManager) {
        try {
            packageManager.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES);
            return true;
        } catch (PackageManager.NameNotFoundException e) {
            return false;
        }
    }

    public static ProgressDialog showProgress(String progressMessage, ProgressDialog aProgressDialog, Activity aParent) { //Context aParent) {
        if(verifyDialog != null && verifyDialog.isShowing()) {
            verifyDialog.dismiss();
            disableRequestTimer(timerHandler);
        }
        if (aProgressDialog != null && aProgressDialog.isShowing()) {
            aProgressDialog.setMessage(progressMessage);
        } else {
            // Note: if 1st parameter is Context not Activity then: WindowManager$BadTokenException: Unable to add window -- token null is not for an application
            //ProgressDialog.show(aParent, null, progressMessage, true, false);
            aProgressDialog = ProgressDialog.show(aParent, null, progressMessage, true, false);
        }
        return aProgressDialog;
    }

    public static void dismissProgress(ProgressDialog aProgressDialog) {
        if(verifyDialog != null && verifyDialog.isShowing()) {
            verifyDialog.dismiss();
            disableRequestTimer(timerHandler);
        }
        if (aProgressDialog != null && aProgressDialog.isShowing()) {
            aProgressDialog.dismiss();
        }
    }

    public static void disableRequestTimer(Handler aHandler) {
        Log.d(TAG, "disableRequestTimer()");
        if (aHandler != null) {
            aHandler.removeCallbacksAndMessages(null);
        }
    }

    public static void enableVerifyPinTimer(Handler aHandler, final Activity aActivity, final ProgressDialog aProgressDialog) {
        Log.d(TAG, "enableVerifyPinTimer()");
        aHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                DcUtils.showErrorDialog(aActivity, "DerivID CP cannot communicate with the DerivID. Make sure you have generated Credentials.");
                DcUtils.dismissProgress(aProgressDialog);
            }
        }, VERIFY_PIN_TIMEOUT);
    }

    public static void enableDcRequestTimer(Handler aHandler, final Activity aActivity, final ProgressDialog aProgressDialog) {
        Log.d(TAG, "enableDcRequestTimer()");
        aHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.w(TAG, aActivity.getString(R.string.dc_request_timeout));
                //Toast.makeText(aActivity, R.string.dc_request_timeout, Toast.LENGTH_SHORT).show();
                DcUtils.showErrorDialog(aActivity, R.string.dc_request_timeout);
                DcUtils.dismissProgress(aProgressDialog);
            }
        }, DC_REQUEST_TIMEOUT);
    }


    private static final IntentFilter INTENT_FILTER = new IntentFilter();
    static {
        INTENT_FILTER.addAction(DcContainerResponseReceiver.ACTION_FORWARDED_ERROR);
        INTENT_FILTER.addAction(DcContainerResponseReceiver.ACTION_FORWARDED_RESPONSE);
    }
    static public ForwardedDcIntentReceiver  registerDCReceiver(Context context, ForwardedDcIntentReceiver  dcReceiver) {
        if (dcReceiver == null) {
            Log.d(TAG, "Creating Forwarded DC Intent Receiver");
            dcReceiver = new ForwardedDcIntentReceiver();
        }
        Log.d(TAG, "Registering Forwarded DC Intent Receiver");
        LocalBroadcastManager.getInstance(context).registerReceiver(dcReceiver, INTENT_FILTER);

        return dcReceiver;
    }
    static public void unregisterDCReceiver(Context context, ForwardedDcIntentReceiver  dcReceiver) {
        Log.d(TAG, "Unregistering Forwarded DC Intent Receiver");
        LocalBroadcastManager.getInstance(context).unregisterReceiver(dcReceiver);
    }


    static public void setDCType(
            Activity activity,
            AlertDialog dialog,
            ForwardedDcIntentReceiver dcReceiver,
            int dcType, //final String credentialType
            VpnProfile profile
    ){
        // debug
        //Toast.makeText(getActivity(), "changed DC type to: " + dcType, Toast.LENGTH_LONG).show();
        String credentialType;
        switch(dcType) {
            case VpnProfile.TYPEDC_TUNNEL:
                credentialType = VpnProfile.TYPE_TUNNEL;
                break;

            case VpnProfile.TYPEDC_PIV_AUTHENTICATION:
                credentialType = VpnProfile.TYPE_PIV_AUTH;
                break;

            case VpnProfile.TYPEDC_DIGITAL_SIGNATURE:
                credentialType = VpnProfile.TYPE_DIGITAL_SIG;
                break;

            default: // TYPEDC_UNDEFINED, it initializes a newly created VPN profile.
                credentialType = null;
                break;
        }
        if(credentialType != null) {
            // If Derived Credentials have not been received and set to the profile
            // then popup the PIN dialog that eventually results in requesting/receiving
            // Derived Credentials and storing them in the profile (via OnRetrieveCredential() callback).
            //TODO: Have the only instance of dcReceiver to be in application class (so far, there are two instances: in VPNProfileList and Settings_Basic).
            String existingPin;
            if(profile.getPrivateKey() != null && profile.getDerivedCertificates(activity).length > 0
                && (existingPin = dcReceiver.getPin()) != null && !existingPin.isEmpty())
            {
                // Getting existing PIN from the dcReceiver instance.
                verifyPin(activity, existingPin, null, dcReceiver, credentialType);
            } else {
                // Getting PIN from user's input.
                showPinInput(activity, dialog, dcReceiver, credentialType);
            }
        }
    }


    // Copied form: com.routeone.dc.hostappdemo.MainActivity#showPinInput
    static public void showPinInput(
            final Activity activity,
            AlertDialog dialog,
            final ForwardedDcIntentReceiver dcReceiver,
            final String credentialType)
    {
        showPinInput(activity, dialog, dcReceiver, credentialType, false);
    }
    @SuppressLint("InflateParams")
    static public void showPinInput(
            final Activity activity,
            AlertDialog dialog,
            final ForwardedDcIntentReceiver dcReceiver,
            final String credentialType,
                boolean importConfigFromMDM)
    {
        //Log.d(TAG, "showPinInput()");
        View viewInflated = activity.getLayoutInflater().inflate(R.layout.dialog_pin_input, null, false);
        final EditText pinEditText  = (EditText) viewInflated.findViewById(R.id.pin_code_input);
        pinEditText.setImeOptions(EditorInfo.IME_ACTION_DONE);
        if(dialog != null)
            dialog.dismiss();
        dialog = new AlertDialog.Builder(activity)
                .setView(viewInflated)
                .setPositiveButton(android.R.string.ok, null)
                .setNegativeButton(android.R.string.cancel, null)
                .setCancelable(false)
                .create();

        pinEditText.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); // disabling the fullscreen input mode for this EditText (does not work in XML layout, why?)

        if(importConfigFromMDM) {
            dialog.setTitle(R.string.pin_dialog_title_profile_from_MDM);
            pinEditText.setHint(activity.getResources().getString(R.string.pin_hint_profile_from_MDM));
        } else {
            dialog.setTitle(R.string.pin_dialog_title_profile_from_container);
            pinEditText.setHint(activity.getResources().getString(R.string.pin_hint_from_container));
        }

        dialog.setOnShowListener(new DialogInterface.OnShowListener() {
            AlertDialog mmDialog;
            DialogInterface.OnShowListener init(AlertDialog dialog) {
                mmDialog=dialog;
                return this;
            }
            @Override
            public void onShow(DialogInterface i) {
                Button positiveBtn = mmDialog.getButton(AlertDialog.BUTTON_POSITIVE);
                positiveBtn.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        if(mmDialog.isShowing()) {
                            ICSOpenVPNApplication.getInstance().pinDialogDoneOnStartup = true;
                            String pin = pinEditText.getText().toString();
                            verifyPin(activity, pin, mmDialog, dcReceiver, credentialType);
                        }
                        mmDialog.dismiss();
                    }
                });
                Button negativeBtn = mmDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
                negativeBtn.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        ICSOpenVPNApplication.getInstance().pinDialogDoneOnStartup = true;
                        mmDialog.dismiss();
                    }
                });
            }
        }.init(dialog));
        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialogInterface) {
                // Do not let keyboard popup on VPN Profile Name field any time when user navigates to a VPN profile's BASIC tab.
                View focus = activity.getCurrentFocus();
                //DcUtils.hideKeyboard(activity, focus != null ? focus : mProfileName);
                if(focus != null) DcUtils.hideKeyboard(activity, focus);
            }
        });
        if (dialog.getWindow() != null) {
            dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
        }
        pinEditText.setOnEditorActionListener(new EditText.OnEditorActionListener() {
            AlertDialog mmDialog;
            TextView.OnEditorActionListener init(AlertDialog dialog) {
                mmDialog=dialog;
                return this;
            }
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_DONE ||
                        event.getAction() == KeyEvent.ACTION_DOWN &&
                                event.getKeyCode() == KeyEvent.KEYCODE_ENTER)
                {
                    String pin = pinEditText.getText().toString();
                    verifyPin(activity, pin, mmDialog, dcReceiver, credentialType);
                    return true;
                }
                return false;
            }
        }.init(dialog));
        dialog.show();
    }
    static public void verifyPin(
            final Activity activity,
            String pin,
            AlertDialog dialog,
            final ForwardedDcIntentReceiver dcReceiver,
            final String credentialType)
    {


            if(pin == null) {
                throw new IllegalArgumentException("PIN is NULL");
            }

            if (!pin.isEmpty()) {
                if(dialog != null && dialog.isShowing()) {
                    dialog.dismiss();
                    verifyDialog = showProgress("Verifying PIN...", null, activity);
                    timerHandler = new Handler();
                    enableVerifyPinTimer(timerHandler, activity, verifyDialog);
                }

                dcReceiver.setContext(activity); //(activity.getApplicationContext());
                dcReceiver.setPin(pin);
                dcReceiver.setCredentialType(credentialType);

                DcContainerInterface.verifyPin(activity, pin);
            } else {
                Log.w(TAG, activity.getString(R.string.pin_cannot_be_empty));
                //Toast.makeText(activity, R.string.pin_cannot_be_empty, Toast.LENGTH_SHORT).show();
                DcUtils.showErrorDialog(activity, R.string.pin_cannot_be_empty);
            }
//        }
    }


    static public void showErrorDialog(
            final Activity activity, // Context context,
            int messageId)
    {
        showErrorDialog(activity, messageId, null);
    }
    static public void showErrorDialog(
            final Activity activity, // Context context,
            String message)
    {
        showErrorDialog(activity, 0, message);
    }

    static public AlertDialog showErrorDialog(
            final Activity activity, // Context context,
            int messageId,
            String message)
    {
        // Note, to avoid the exception in AlertDialog.show(), I have to use an Activity instance, not (application's) Context:
        // WindowManager$BadTokenException: Unable to add window -- token null is not for an application
        if(verifyDialog != null && verifyDialog.isShowing()) {
            verifyDialog.dismiss();
            disableRequestTimer(timerHandler);
        }
        AlertDialog.Builder b = new AlertDialog.Builder(activity);
        b.setTitle(R.string.app_name); //TODO? need to define a common DC error title? //(R.string.config_error_found);
        if(messageId > 0 && message != null) {
            b.setMessage(activity.getString(messageId, message));
        } else if(messageId > 0) {
            b.setMessage(messageId);
        } else if(message != null) {
            b.setMessage(message);
        } else {
            throw new IllegalArgumentException("Message is not specified");
        }
        b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                //activity.finish();
            }
        });
        b.setOnCancelListener(new DialogInterface.OnCancelListener() {
            @Override
            public void onCancel(DialogInterface dialog) {
                //activity.finish();
            }
        });
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
            setOnDismissListener(activity, b);
        }
        if(errorDialog != null && errorDialog.isShowing())
            errorDialog.dismiss();
        errorDialog = b.show();

        setAlertDialogTitleAndDividerColorOnKitkat(errorDialog);

        return errorDialog;
    }
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    static private void setOnDismissListener(
            final Activity activity, // Context context,
            AlertDialog.Builder d)
    {
        d.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                //activity.finish();
            }
        });
    }

    public static void setAlertDialogTitleAndDividerColorOnKitkat(AlertDialog dialog) {
        // For Android API 4.4, set alert dialog title's and divider's color.
        if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT_WATCH) {
            // Set title divider color // See: http://blog.supenta.com/2014/07/02/how-to-style-alertdialogs-like-a-pro/
            int titleDividerId = dialog.getContext().getResources().getIdentifier("titleDivider", "id", "android");
            //or//titleDividerId = d.getContext().getResources().getIdentifier("android:id/titleDivider", null, null);
            View titleDivider = dialog.findViewById(titleDividerId);
            if (titleDivider != null) {
                titleDivider.setBackgroundColor(dialog.getContext().getResources().getColor(R.color.Primary_Black));
            }
            // Set title color // See: http://kechengpuzi.com/q/s14439538
            int textViewId = dialog.getContext().getResources().getIdentifier("android:id/alertTitle", null, null);
            TextView tv = (TextView) dialog.findViewById(textViewId);
            tv.setTextColor(dialog.getContext().getResources().getColor(R.color.Primary_Black));
        }
    }

    public static void hideKeyboard(Context ctx, View v) {
        InputMethodManager imm = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow( v.getWindowToken(), 0 );
    }

    public static void showKeyboard(Context ctx, View v) {
        InputMethodManager imm = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.showSoftInput( v, 0 );
    }

    public static void copyFile(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[1024];
        int read;
        while((read = in.read(buffer)) != -1){
            out.write(buffer, 0, read);
        }
    }
    public static boolean isFileExists(Context context, String fileName)
    {
        File[] files = context.getFilesDir().listFiles();
        for(File file: files)
        {
            if(fileName.equalsIgnoreCase(file.getName()))
                return true;

        }
        return false;
    }
    public static void copyAssetToFilesDir(Context context, String fileToCopy)
    {
        AssetManager assetManager = context.getResources().getAssets();
        InputStream in = null;
        OutputStream out = null;
        String fileName = fileToCopy;
        if(isFileExists(context, fileToCopy))
            return;
        try {
            in = assetManager.open(fileToCopy);
            if(in != null) {
                File outFile = new File(context.getFilesDir(), fileToCopy);
                out = new FileOutputStream(outFile);
                copyFile(in, out);
            }
        } catch(IOException e) {
            Log.e("tag", "Failed to copy asset file: " + fileName, e);
        }
    }

}
