package info.knacki.pass.io;

import android.content.Context;
import android.content.res.Resources;

import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.CompressionAlgorithmTags;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
import org.bouncycastle.openpgp.PGPDataValidationException;
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPPBEEncryptedData;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator;
import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
import org.bouncycastle.util.io.Streams;

import java.io.ByteArrayOutputStream;
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.security.SecureRandom;
import java.security.Security;
import java.util.Date;

import info.knacki.gitdroid.callback.OnResponseListener;
import info.knacki.pass.R;
import info.knacki.pass.settings.SettingsManager;

class PasswordFileInterface implements IFileInterface {
    public static final String PASSWORD_SUFFIX = ".pwd";
    protected final File fFile;
    protected final FileInterfaceFactory.PasswordGetter fPasswordGetter;
    private final char[] fPassword;
    private final String fMethodName;

    PasswordFileInterface(Context ctx, FileInterfaceFactory.PasswordGetter passwordGetter, File f) {
        fFile = f;
        fPasswordGetter = passwordGetter;
        fPassword = SettingsManager.GetPassword(ctx).toCharArray();
        Resources r = ctx.getResources();
        fMethodName = r.getString(R.string.pref_enc_type_title_password);
    }

    private boolean TryPassword(String pass, OnResponseListener<InputStream> onResponse) {
        try {
            InputStream in = PGPUtil.getDecoderStream(new FileInputStream(fFile));
            JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in);
            Object o = pgpF.nextObject();
            PGPEncryptedDataList enc = (o instanceof PGPEncryptedDataList) ? (PGPEncryptedDataList) o : (PGPEncryptedDataList) pgpF.nextObject();
            InputStream clear = ((PGPPBEEncryptedData) enc.get(0)).getDataStream(new JcePBEDataDecryptorFactoryBuilder(new JcaPGPDigestCalculatorProviderBuilder().setProvider("BC").build()).setProvider("BC").build(pass.toCharArray()));

            onResponse.OnResponse(clear);
            return true;
        }
        catch (PGPDataValidationException e) {
            // Wrong password
            return false;
        }
        catch (IOException | PGPException e) {
            onResponse.OnError(e.getMessage(), e);
            return true;
        }
    }

    private void FindPassword(final OnResponseListener<InputStream> onResponse) {
        FileInterfaceFactory.OnPasswordEnteredListener onResp = new FileInterfaceFactory.OnPasswordEnteredListener() {
            @Override
            public boolean OnResponse(String result) {
                if (result == null) {
                    onResponse.OnError("Invalid password", null);
                    return false;
                } else if (!TryPassword(result, onResponse)) {
                    fPasswordGetter.GetPassword(this);
                    return false;
                }
                return true;
            }

            @Override
            public void OnError(String msg, Throwable e) {
                onResponse.OnError(msg, e);
            }
        };
        fPasswordGetter.GetPassword(onResp);
    }

    private void DecryptFile(final OnResponseListener<byte[]> resp) {

        FindPassword(new OnResponseListener<InputStream>() {
            @Override
            public void OnResponse(InputStream clear) {
                try {
                    JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(clear);
                    PGPCompressedData cData = (PGPCompressedData) pgpFact.nextObject();
                    pgpFact = new JcaPGPObjectFactory(cData.getDataStream());
                    PGPLiteralData ld = (PGPLiteralData) pgpFact.nextObject();
                    resp.OnResponse(Streams.readAll(ld.getInputStream()));
                }
                catch (IOException |PGPException e) {
                    resp.OnError(e.getMessage(), e);
                }
            }

            @Override
            public void OnError(String msg, Throwable e) {
                resp.OnError(msg, e);
            }
        });
    }

    private void CryptFile(OutputStream fileOutStream, byte[] data) throws IOException, PGPException {
        ByteArrayOutputStream compressedDataStream = new ByteArrayOutputStream();
        PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(CompressionAlgorithmTags.UNCOMPRESSED);
        OutputStream cos = comData.open(compressedDataStream);
        PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator();

        OutputStream  pOut = lData.open(cos, // the compressed output stream
                PGPLiteralData.BINARY,
                PGPLiteralData.CONSOLE,  // "filename" to store
                data.length, // length of clear data
                new Date()  // current time
        );

        pOut.write(data);
        pOut.close();
        comData.close();

        byte []compressedData = compressedDataStream.toByteArray();
        OutputStream out = new ArmoredOutputStream(fileOutStream);

        PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator(new JcePGPDataEncryptorBuilder(PGPEncryptedDataGenerator.AES_128).setWithIntegrityPacket(true).setSecureRandom(new SecureRandom()).setProvider("BC"));
        encGen.addMethod(new JcePBEKeyEncryptionMethodGenerator(fPassword).setProvider("BC"));
        OutputStream encOut = encGen.open(fileOutStream, compressedData.length);

        encOut.write(compressedData);
        encOut.close();
        out.close();
        fileOutStream.close();
    }

    @Override
    public void ReadFile(final OnResponseListener<String> resp) {
        Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
        Security.addProvider(new BouncyCastleProvider());

        if (fFile.length() == 0) {
            resp.OnResponse("");
            return;
        }
        try {
            DecryptFile(new OnResponseListener<byte[]>() {
                @Override
                public void OnResponse(byte[] result) {
                    resp.OnResponse(CharsetHelper.ByteArrayToString(result));
                }

                @Override
                public void OnError(String msg, Throwable e) {
                    resp.OnError(msg, e);
                }
            });
        } catch (Throwable e) {
            resp.OnError(e.getMessage(), e);
        }
    }

    @Override
    public void WriteFile(String content, OnResponseListener<Void> resp) throws InvalidPasswordException {
        if (fPassword.length == 0) {
            throw new InvalidPasswordException("Password not set. Please set password first");
        }

        Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
        Security.addProvider(new BouncyCastleProvider());

        try {
            CryptFile(new FileOutputStream(fFile), CharsetHelper.StringToByteArray(content));
        }
        catch (Throwable e) {
            resp.OnError(e.getMessage(), e);
            return;
        }
        resp.OnResponse(null);
    }

    @Override
    public String GetMethodName() {
        return fMethodName;
    }
}
