use crate::error::{KeyringError, Result};
use crate::*;
use jni::errors::Error;
use jni::errors::Result as JniResult;
use jni::objects::{Global, JClass, JObject, JString, JValue, JValueOwned};
use jni::{jni_sig, jni_str, Env, JavaVM};

///////////////////////////////////////////////////////////

macro_rules! check_exception {
    ($env:expr, $err:expr, $message:expr) => {
        if $env.exception_check() {
            let _ = $env.exception_clear();
            $err = Some(KeyringError::AndroidKeystoreError($message.to_owned()));
            return Err(Error::JavaException);
        }
    };
}

///////////////////////////////////////////////////////////

pub type AndroidKeyringContext = (JavaVM, Global<JObject<'static>>);

pub struct AndroidKeyringManager {
    vm: JavaVM,
    encrypted_shared_preferences: Global<JObject<'static>>,
    _class_loader: Global<JObject<'static>>,
}

impl Drop for AndroidKeyringManager {
    fn drop(&mut self) {
        // Ensure we're attached before dropping GlobalRef
        self.vm
            .attach_current_thread(|_| JniResult::Ok(()))
            .unwrap();
    }
}

impl AndroidKeyringManager {
    pub fn new(application: &str, ctx: AndroidKeyringContext) -> Result<AndroidKeyringManager> {
        let vm = ctx.0;
        let context = ctx.1.as_obj();
        let application = application.to_owned();

        let (encrypted_shared_preferences, class_loader) = vm
            .attach_current_thread(|env| {
                env.with_local_frame(256, |jnienv| {
                    let class_loader = get_class_loader(jnienv, context)?;
                    let master_key = get_master_key(jnienv, context, &class_loader)?;

                    let encrypted_shared_preferences = get_encrypted_shared_preferences(
                        jnienv,
                        application,
                        context,
                        &class_loader,
                        &master_key,
                    )?;

                    JniResult::Ok((
                        jnienv.new_global_ref(encrypted_shared_preferences)?,
                        jnienv.new_global_ref(class_loader)?,
                    ))
                })
            })
            .map_err(|e| KeyringError::Generic(format!("{}", e)))?;

        Ok(AndroidKeyringManager {
            vm,
            encrypted_shared_preferences,
            _class_loader: class_loader,
        })
    }

    pub fn with_keyring<F, T>(&self, service: &str, key: &str, func: F) -> Result<T>
    where
        F: FnOnce(&mut dyn Keyring) -> Result<T>,
    {
        self.vm
            .attach_current_thread(|env| {
                let mut kr = AndroidKeyring::new(self, env, service, key);
                JniResult::Ok(func(&mut kr))
            })
            .map_err(|e| KeyringError::Generic(format!("{}", e)))?
    }
}

pub struct AndroidKeyring<'a, 'b> {
    manager: &'a AndroidKeyringManager,
    jnienv: &'a mut Env<'b>,
    service: &'a str,
    key: &'a str,
}

impl<'a, 'b> AndroidKeyring<'a, 'b> {
    fn new(
        manager: &'a AndroidKeyringManager,
        jnienv: &'a mut Env<'b>,
        service: &'a str,
        key: &'a str,
    ) -> AndroidKeyring<'a, 'b> {
        AndroidKeyring {
            manager,
            jnienv,
            service,
            key,
        }
    }

    fn make_key_string(&self) -> String {
        format!(
            "{}\t{}",
            crate::escape(self.service),
            crate::escape(self.key)
        )
    }
}

impl<'a, 'b> Keyring for AndroidKeyring<'a, 'b> {
    fn set_value(&mut self, value: &str) -> Result<()> {
        let mut err: Option<KeyringError> = None;
        let key_string = self.make_key_string();
        let encrypted_shared_preferences = self.manager.encrypted_shared_preferences.as_obj();
        let out = self.jnienv.with_local_frame(256, |jnienv| {
            let editor = jnienv
                .call_method(
                    encrypted_shared_preferences,
                    jni_str!("edit"),
                    jni_sig!("()Landroid/content/SharedPreferences$Editor;"),
                    &[],
                )?
                .l()?;
            check_exception!(jnienv, err, "Couldn't edit shared preferences");

            let k = get_string_jvalue(jnienv, &key_string)?;
            let v = get_string_jvalue(jnienv, &crate::escape(value))?;
            let editor = jnienv
                .call_method(
                    editor,
                    jni_str!("putString"),
                    jni_sig!("(Ljava/lang/String;Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;"),
                    &[ k.borrow(), v.borrow() ],
                )
                ?
                .l()
                ?;
            check_exception!(jnienv, err, "Couldn't put string to shared preferences");

            let _ = jnienv.call_method(editor, jni_str!("apply"), jni_sig!("()V"), &[])?;
            check_exception!(jnienv, err, "Couldn't put apply changes to shared preferences");
            Ok(())
        });

        if let Some(err) = err {
            return Err(err);
        }

        out.map_err(|e| KeyringError::AndroidKeystoreError(format!("{}", e)))
    }

    fn get_value(&self) -> Result<String> {
        let mut err: Option<KeyringError> = None;
        let key_string = self.make_key_string();
        let encrypted_shared_preferences = self.manager.encrypted_shared_preferences.as_obj();
        let out = self.jnienv.with_local_frame(256, |jnienv| {
            let k = get_string_jvalue(jnienv, &key_string)?;
            let exists = jnienv
                .call_method(
                    self.manager.encrypted_shared_preferences.as_obj(),
                    jni_str!("contains"),
                    jni_sig!("(Ljava/lang/String;)Z"),
                    &[k.borrow()],
                )?
                .z()?;
            check_exception!(
                jnienv,
                err,
                "Couldn't check existence of string from shared preferences"
            );

            if !exists {
                err = Some(KeyringError::NoPasswordFound);
                return Ok(String::new());
            }

            let value = jnienv
                .call_method(
                    encrypted_shared_preferences,
                    jni_str!("getString"),
                    jni_sig!("(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"),
                    &[k.borrow(), JValue::Object(&JObject::null())],
                )?
                .l()?;
            check_exception!(jnienv, err, "Couldn't get string from shared preferences");

            let jstr = JString::cast_local(jnienv, value)?;
            Ok(jstr.to_string())
        });

        if let Some(err) = err {
            return Err(err);
        }

        let out = out.map_err(|e| KeyringError::AndroidKeystoreError(format!("{}", e)))?;
        let out = crate::unescape(&out).map_err(map_to_generic)?;

        Ok(out)
    }

    fn delete_value(&mut self) -> Result<()> {
        let mut err: Option<KeyringError> = None;
        let key_string = self.make_key_string();
        let encrypted_shared_preferences = self.manager.encrypted_shared_preferences.as_obj();
        let out = self.jnienv.with_local_frame(256, |jnienv| {
            let k = get_string_jvalue(jnienv, &key_string)?;
            let exists = jnienv
                .call_method(
                    encrypted_shared_preferences,
                    jni_str!("contains"),
                    jni_sig!("(Ljava/lang/String;)Z"),
                    &[k.borrow()],
                )?
                .z()?;
            check_exception!(
                jnienv,
                err,
                "Couldn't check existence of string from shared preferences"
            );

            if !exists {
                err = Some(KeyringError::NoPasswordFound);
                return Ok(());
            }

            let editor = jnienv
                .call_method(
                    encrypted_shared_preferences,
                    jni_str!("edit"),
                    jni_sig!("()Landroid/content/SharedPreferences$Editor;"),
                    &[],
                )?
                .l()?;
            check_exception!(jnienv, err, "Couldn't edit shared preferences");

            let editor = jnienv
                .call_method(
                    editor,
                    jni_str!("remove"),
                    jni_sig!("(Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;"),
                    &[k.borrow()],
                )?
                .l()?;
            check_exception!(
                jnienv,
                err,
                "Couldn't remove string from shared preferences"
            );

            let _ = jnienv.call_method(editor, jni_str!("apply"), jni_sig!("()V"), &[])?;
            check_exception!(
                jnienv,
                err,
                "Couldn't put apply changes to shared preferences"
            );

            Ok(())
        });

        if let Some(err) = err {
            return Err(err);
        }

        out.map_err(|e| KeyringError::AndroidKeystoreError(format!("{}", e)))
    }
}

///////////////////////////////////////////////////////////

pub fn get_string_jvalue<'a>(jnienv: &mut Env<'a>, s: &str) -> JniResult<JValueOwned<'a>> {
    Ok(JValueOwned::from(JObject::from(jnienv.new_string(s)?)))
}

pub fn get_class_loader<'a>(jnienv: &mut Env<'a>, context: &JObject<'a>) -> JniResult<JObject<'a>> {
    jnienv
        .call_method(
            context,
            jni_str!("getClassLoader"),
            jni_sig!("()Ljava/lang/ClassLoader;"),
            &[],
        )?
        .l()
}

pub fn load_class<'a, 'b>(
    jnienv: &mut Env<'a>,
    class_loader: &JObject<'b>,
    name: &str,
) -> JniResult<JClass<'a>> {
    let name = get_string_jvalue(jnienv, name)?;
    let class = jnienv
        .call_method(
            class_loader,
            jni_str!("loadClass"),
            jni_sig!("(Ljava/lang/String;)Ljava/lang/Class;"),
            &[name.borrow()],
        )?
        .l()?;

    JClass::cast_local(jnienv, class)
}

pub fn get_master_key<'b>(
    jnienv: &mut Env<'b>,
    context: &JObject<'b>,
    class_loader: &JObject<'b>,
) -> JniResult<JObject<'b>> {
    // KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
    //     MASTER_KEY_ALIAS,
    //     KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
    //     .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    //     .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    //     .setKeySize(KEY_SIZE)
    //     .build();
    jnienv.with_local_frame_returning_local::<_, JObject, _>(64, |jnienv| {
        let c_kgps_builder = load_class(jnienv, class_loader, "android/security/keystore/KeyGenParameterSpec$Builder")?;
        let c_master_key = load_class(jnienv, class_loader, "androidx/security/crypto/MasterKey")?;
        let c_key_properties = load_class(jnienv, class_loader,"android/security/keystore/KeyProperties")?;
        let c_string = load_class(jnienv, class_loader, "java/lang/String")?;
        let c_master_key_builder = load_class(jnienv, class_loader, "androidx/security/crypto/MasterKey$Builder")?;

        let sf_dmka = jnienv.get_static_field(
            &c_master_key,
            jni_str!("DEFAULT_MASTER_KEY_ALIAS"),
            jni_sig!("Ljava/lang/String;"),
        )?;
        let sf_pe =jnienv.get_static_field(
            &c_key_properties,
            jni_str!("PURPOSE_ENCRYPT"),
            jni_sig!("I"),
        )?;
        let sf_pd = jnienv
        .get_static_field(
            &c_key_properties,
            jni_str!("PURPOSE_DECRYPT"),
            jni_sig!("I"),
        )?;
        let builder = jnienv
            .new_object(
                c_kgps_builder,
                jni_sig!("(Ljava/lang/String;I)V"),
                &[
                    sf_dmka.borrow(),
                    JValue::Int(sf_pe.i()? | sf_pd.i()?),
                ],
            )
            ?;
        let sf_bm_gcm = jnienv.get_static_field(
            &c_key_properties,
            jni_str!("BLOCK_MODE_GCM"),
            jni_sig!("Ljava/lang/String;"),
        )?.l()?;
        let sbm_params = jnienv.new_object_array(
            1,
            &c_string,
            sf_bm_gcm,
        )?;
        let builder = jnienv
            .call_method(
                builder,
                jni_str!("setBlockModes"),
                jni_sig!("([Ljava/lang/String;)Landroid/security/keystore/KeyGenParameterSpec$Builder;"),
                &[JValue::from(&*sbm_params)],
            )?.l()?;

        let sf_epn = jnienv.get_static_field(
                &c_key_properties,
                jni_str!("ENCRYPTION_PADDING_NONE"),
                jni_sig!("Ljava/lang/String;"),
            )?;
        let sep_params = jnienv.new_object_array(
                1,
                &c_string,
                sf_epn.l()?,
            )?;
        let builder = jnienv
            .call_method(
                builder,
                jni_str!("setEncryptionPaddings"),
                jni_sig!("([Ljava/lang/String;)Landroid/security/keystore/KeyGenParameterSpec$Builder;"),
                &[JValue::from(&*sep_params)],
            )?.l()?;
        let sf_dagmks = jnienv
            .get_static_field(
                &c_master_key,
                jni_str!("DEFAULT_AES_GCM_MASTER_KEY_SIZE"),
                jni_sig!("I"),
            )?;
        let builder = jnienv
            .call_method(
                builder,
                jni_str!("setKeySize"),
                jni_sig!("(I)Landroid/security/keystore/KeyGenParameterSpec$Builder;"),
                &[sf_dagmks.borrow()],
            )?.l()?;
        let spec = jnienv
            .call_method(
                builder,
                jni_str!("build"),
                jni_sig!("()Landroid/security/keystore/KeyGenParameterSpec;"),
                &[],
            )?;
        // return new MasterKey.Builder(MainActivity.this)
        //      .setKeyGenParameterSpec(spec)
        //      .build();
        let mkbuilder = jnienv
            .new_object(
                &c_master_key_builder,
                jni_sig!("(Landroid/content/Context;)V"),
                &[JValue::from(context)],
            )?;
        let mkbuilder = jnienv.call_method(mkbuilder,
            jni_str!("setKeyGenParameterSpec"), 
            jni_sig!("(Landroid/security/keystore/KeyGenParameterSpec;)Landroidx/security/crypto/MasterKey$Builder;"), 
            &[ spec.borrow() ])?.l()?;
        let master_key = jnienv
            .call_method(
                mkbuilder,
                jni_str!("build"),
                jni_sig!("()Landroidx/security/crypto/MasterKey;"),
                &[],
            )?.l()?;
        Ok(master_key)
    })
}

pub fn get_encrypted_shared_preferences<'b>(
    jnienv: &mut Env<'b>,
    application: String,
    context: &JObject<'b>,
    class_loader: &JObject<'b>,
    master_key: &JObject<'b>,
) -> JniResult<JObject<'b>> {
    jnienv.with_local_frame_returning_local::<_, JObject, _>(64, |jnienv| {
        let c_esp = load_class(jnienv, class_loader, "androidx/security/crypto/EncryptedSharedPreferences")?;
        let c_esp_pkes = load_class(
            jnienv,
            class_loader,
            "androidx/security/crypto/EncryptedSharedPreferences$PrefKeyEncryptionScheme")?;
        let c_esp_pves =load_class(
            jnienv,
            class_loader,
            "androidx/security/crypto/EncryptedSharedPreferences$PrefValueEncryptionScheme")?;    
        let application = get_string_jvalue(jnienv, &application)?;
        let sf_pkes = jnienv.get_static_field(
            c_esp_pkes,
            jni_str!("AES256_SIV"), 
            jni_sig!("Landroidx/security/crypto/EncryptedSharedPreferences$PrefKeyEncryptionScheme;"))?;
        let sf_pves = jnienv.get_static_field(
            c_esp_pves,
            jni_str!("AES256_GCM"), 
            jni_sig!("Landroidx/security/crypto/EncryptedSharedPreferences$PrefValueEncryptionScheme;"))?;
        let esp = jnienv.call_static_method(
            &c_esp,
            jni_str!("create"), 
            jni_sig!("(Landroid/content/Context;Ljava/lang/String;Landroidx/security/crypto/MasterKey;Landroidx/security/crypto/EncryptedSharedPreferences$PrefKeyEncryptionScheme;Landroidx/security/crypto/EncryptedSharedPreferences$PrefValueEncryptionScheme;)Landroid/content/SharedPreferences;"),
            &[
                JValue::from(context),
                application.borrow(),
                JValue::Object(master_key),
                sf_pkes.borrow(),
                sf_pves.borrow()
            ])?.l()?;
        Ok(esp)
    })
}
