package com.secspace.lib.common.utils;

/**
 * Created by SKR-CYW on 2017/7/13.
 */

import android.content.ContentResolver;
import android.content.Context;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Handler;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.util.Log;

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 javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * @author hyw
 * @since 2017/3/9
 */
public class FileCryptHelper {

    private static final String TAG = "FileCryptHelper";
    public static final byte FILE_ENCRYPTED_FLAG = 0xa;

    public interface ProgressListener {
        void onProgress(int progress, boolean encrypt);
    }

    private static final String KEY_ALGORITHM = "AES";
    private static final String CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding";
    private static final String ECB_PKCS5_PADDING = "AES/ECB/PKCS5Padding";
    private static final String CFB_NO_PADDING = "AES/CFB/ZeroBytePadding";
    // 分组长度固定为128比特，密钥长度只有128、192、和256比特三种。
    private static final String KEY = "M2D0AfMqvRvv7Sum";

    public static boolean encrypt(Context context, String inPath, String outPath, boolean deleteSource, Handler handler, final ProgressListener progressListener) {
        File inFile = new File(inPath);
        if (!inFile.exists()) {
            Log.i(TAG, "encrypt file not exist!");
            return false;
        }
        IvParameterSpec iv = new IvParameterSpec(KEY.substring(0, 16).getBytes());
        SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), KEY_ALGORITHM);

        InputStream is = null;
        OutputStream out = null;
        CipherInputStream cis = null;

        File toFile = ensureFile(outPath);
        try {
            Cipher cipher = Cipher.getInstance(CFB_NO_PADDING);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
            is = new FileInputStream(inFile);
            out = new FileOutputStream(toFile);
            cis = new CipherInputStream(is, cipher);

            final int totalLength = is.available();
            byte[] buffer = new byte[1024];
            int size;
            int readLength = 0;
            boolean hadAddEncryptFlag = false;
            while ((size = cis.read(buffer)) > 0) {
                if (!hadAddEncryptFlag) {
                    out.write(0xa);
                    out.write(0xa);
                    hadAddEncryptFlag = true;
                }
                out.write(buffer, 0, size);
                if (progressListener != null) {
                    readLength += size;
                    final int finalReadLength = readLength;
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            progressListener.onProgress((int) (((float) finalReadLength * 1.0 / totalLength) * 100), true);
                        }
                    });
                }
            }

            if (deleteSource) {
                boolean isDeleteSuccess = inFile.delete();
                if (isDeleteSuccess) {
                    deleteFromMediaStore(context, inPath);
                } else {
                    Log.e(TAG, "encrypt deleteFromMediaStore failed");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            Log.i(TAG, e.toString());
            return false;
        } finally {
            try {
                if (cis != null) {
                    cis.close();
                }
                if (is != null) {
                    is.close();
                }
                if (out != null) {
                    out.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    public static boolean decrypt(Context context, String inPath, String outPath, boolean deleteSource, Handler handler, final ProgressListener progressListener) {
        File inFile = new File(inPath);
        if (!inFile.exists()) {
            Log.i(TAG, "decrypt file not exist!");
            return false;
        }
        IvParameterSpec iv = new IvParameterSpec(KEY.substring(0, 16).getBytes());
        SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), KEY_ALGORITHM);

        InputStream is = null;
        OutputStream out = null;
        CipherOutputStream cos = null;

        File toFile = ensureFile(outPath);
        try {
            Cipher cipher = Cipher.getInstance(CFB_NO_PADDING);
            cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
            is = new FileInputStream(inFile);
            byte[] input2byte = input2byte(is);
            final byte[] temp = new byte[input2byte.length - 2];
            System.arraycopy(input2byte, 2, temp, 0, temp.length);
            out = new FileOutputStream(toFile);
            cos = new CipherOutputStream(out, cipher);
            byte[] buffer = new byte[1024];
            int size;
            int readLength = 0;
            while ((size = is.read(buffer)) >= 0) {
                cos.write(buffer, 0, size);
                if (progressListener != null) {
                    readLength += size;
                    final int finalReadLength = readLength;
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            progressListener.onProgress((int) (((float) finalReadLength * 1.0 / temp.length) * 100), false);
                        }
                    });
                }
            }
            if (deleteSource) {
                boolean isDeleteSuccess = inFile.delete();
                if (isDeleteSuccess) {
                    deleteFromMediaStore(context, outPath);
                } else {
                    Log.e(TAG, "decrypt deleteFromMediaStore failed");
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "decrypt Exception: " + e.toString());
            e.printStackTrace();
            return false;
        } finally {
            try {
                if (cos != null) {
                    cos.close();
                }
                if (out != null) {
                    out.close();
                }
                if (is != null) {
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    @NonNull
    private static File ensureFile(String path) {
        File file = new File(path);
        if (!file.getParentFile().exists()) {
            file.getParentFile().mkdirs();
        }
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return file;
    }

    /**
     * 添加MediaStore记录
     */
    public static void addToMediaStore(Context context, String path) {
        scanFile(context, path, false);
    }

    /**
     * 扫描指定路径的文件，并添加到数据库或从数据库中移除
     *
     * @param isDelete true：删除；false：添加
     */
    private static void scanFile(final Context context, String path, final boolean isDelete) {
        try {
            MediaScannerConnection.scanFile(context, new String[]{path}, null, new MediaScannerConnection.OnScanCompletedListener() {
                @Override
                public void onScanCompleted(String path, Uri uri) {
                    if (isDelete && uri != null) {
                        context.getContentResolver().delete(uri, null, null);
                    }
                }
            });
        } catch (Exception e) {
            Log.e(TAG, "scanFile  Exception: " + e.toString());
            e.printStackTrace();
        }
    }

    /**
     * 删除文件数据库中的记录，不然delete文件后虽然文件已经不存在了，但是还是会显示出来
     */
    public static void deleteFromMediaStore(Context context, String path) {
        ContentResolver contentResolver = context.getApplicationContext().getContentResolver();
        File file = new File(path);
        String canonicalPath;
        try {
            canonicalPath = file.getCanonicalPath();
        } catch (IOException e) {
            canonicalPath = file.getAbsolutePath();
        }
        String selection = MediaStore.Files.FileColumns.DATA + " = ?";
        String[] selectionArgs = new String[]{canonicalPath};
        final Uri uri = MediaStore.Files.getContentUri("external");
        final int result = contentResolver.delete(uri, selection, selectionArgs);
        if (result == 0) {
            final String absolutePath = file.getAbsolutePath();
            if (!absolutePath.equals(canonicalPath)) {
                contentResolver.delete(uri, selection, selectionArgs);
            }
        }
    }

    /**
     * 获取文件头判断是否加密
     *
     * @param path
     * @return
     */
    public static boolean isEncrypted(String path) {
        FileInputStream is = null;
        boolean isEncrypted = false;
        try {
            is = new FileInputStream(path);
            byte[] b = new byte[2];
            is.read(b, 0, 2);
            if (b[0] == FILE_ENCRYPTED_FLAG && b[1] == FILE_ENCRYPTED_FLAG)
                isEncrypted = true;
        } catch (Exception e) {
            Log.e(TAG, "isEncrypted error : " + e.toString());
        } finally {
            if (null != is) {
                try {
                    is.close();
                } catch (IOException e) {
                }
            }
        }
        return isEncrypted;
    }

    private static final byte[] input2byte(InputStream inStream) throws IOException {
        ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
        byte[] buff = new byte[1024];
        int rc = 0;
        while ((rc = inStream.read(buff, 0, 1024)) > 0) {
            swapStream.write(buff, 0, rc);
        }
        inStream.close();
        return swapStream.toByteArray();
    }
}