/*
 * Copyright 2017-2020 Pranav Pandey
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.pranavpandey.android.dynamic.utils;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URI;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

/**
 * Helper class to perform various file operations.
 *
 * <p><p>A {@link FileProvider} in the form of {@code ${applicationId}.FileProvider} must be
 * added in the {@code manifest} to perform some operations automatically like saving the
 * bitmap or file in app isolated directory.
 *
 * @see Context#getExternalFilesDir(String)
 */
public class DynamicFileUtils {

    /**
     * Default suffix for the file provider.
     */
    private static final String FILE_PROVIDER = ".FileProvider";

    /**
     * Constant to match the content uri.
     */
    private static final String URI_MATCHER_CONTENT = "content:";

    /**
     * Constant to match the file uri.
     */
    private static final String URI_MATCHER_FILE = "file:";

    /**
     * Constant for the default data directory.
     */
    public static final String ADU_DEFAULT_DIR_DATA = "data";

    /**
     * Constant for the default temp directory.
     */
    public static final String ADU_DEFAULT_DIR_TEMP = "temp";

    /**
     * Constant for the {@code application/octet-stream} mime type.
     */
    public static final String ADU_MIME_OCTET_STREAM = "application/octet-stream";

    /**
     * Returns the default {@code temp} directory for a context.
     *
     * @param context The context to get the package name.
     */
    public static @Nullable String getTempDir(@NonNull Context context) {
        if (context.getExternalFilesDir(null) == null) {
            return null;
        }

        return context.getExternalFilesDir(null).getPath()
                + File.separator + ADU_DEFAULT_DIR_DATA
                + File.separator + context.getPackageName()
                + File.separator + ADU_DEFAULT_DIR_TEMP + File.separator;
    }

    /**
     * Returns the base name without extension of given file name.
     * <p>e.g. getBaseName("file.txt") will return "file".
     *
     * @param fileName The full name of the file with extension.
     *
     * @return The base name of the file without extension.
     */
    public static @Nullable String getBaseName(@Nullable String fileName) {
        if (fileName == null) {
            return null;
        }

        int index = fileName.lastIndexOf('.');
        if (index == -1) {
            return fileName;
        } else {
            return fileName.substring(0, index);
        }
    }

    /**
     * Returns the extension of a file name.
     *
     * @param fileName The file name to retrieve its extension.
     *
     * @return The extension of the file name.
     */
    public static @Nullable String getExtension(@Nullable String fileName) {
        if (fileName == null) {
            return null;
        }

        String extension = null;
        int i = fileName.lastIndexOf('.');

        if (i > 0 && i < fileName.length() - 1) {
            extension = fileName.substring(i + 1).toLowerCase();
        }

        return extension;
    }

    /**
     * Returns the extension of a file.
     *
     * @param file The file to retrieve its extension.
     *
     * @return The extension of the file.
     */
    public static @Nullable String getExtension(@Nullable File file) {
        if (file == null) {
            return null;
        }

        return getExtension(file.getName());
    }

    /**
     * Verifies a file if it exist or not.
     *
     * @param file The file to be verified.
     *
     * @return {@code true} if a file can be accessed by automatically creating the
     *         sub directories.
     */
    public static boolean verifyFile(@Nullable File file) {
        if (file == null) {
            return false;
        }

        boolean fileExists = file.exists();
        if (!fileExists) {
            fileExists = file.mkdirs();
        }

        return fileExists;
    }

    /**
     * Delete a directory.
     *
     * @param dir The directory to be deleted.
     *
     * @return {@code true} if the directory has been deleted successfully.
     */
    public static boolean deleteDirectory(@NonNull File dir) {
        if (dir.isDirectory()) {
            String[] children = dir.list();

            if (children != null) {
                for (String aChildren : children) {
                    boolean success = deleteDirectory(new File(dir, aChildren));
                    if (!success) {
                        return false;
                    }
                }
            }
        }

        return dir.delete();
    }

    /**
     * Creates a zip archive from the directory.
     *
     * @param dir The directory to be archived.
     * @param zip The output zip archive.
     *
     * @throws IOException Throws IO exception.
     */
    public static void zipDirectory(@NonNull File dir, @NonNull File zip) throws IOException {
        if (zip.getParent() != null && verifyFile(new File(zip.getParent()))) {
            ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip));
            zip(dir, dir, zos);
            zos.close();
        }
    }

    /**
     * Creates a zip archive from the zip output stream.
     *
     * @param dir The directory to be archived.
     * @param zip The output zip archive.
     * @param zos The zip output stream.
     */
    private static void zip(@NonNull File dir, @NonNull File zip,
            @NonNull ZipOutputStream zos) throws IOException {
        File[] files = dir.listFiles();
        byte[] buffer = new byte[8192];
        int read;

        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    zip(file, zip, zos);
                } else {
                    FileInputStream in = new FileInputStream(file);
                    ZipEntry entry = new ZipEntry(file.getPath().substring(
                            zip.getPath().length() + 1));
                    zos.putNextEntry(entry);

                    while (-1 != (read = in.read(buffer))) {
                        zos.write(buffer, 0, read);
                    }

                    in.close();
                }
            }
        }
    }

    /**
     * Extracts a zip archive.
     *
     * @param zip The zip archive to be extracted.
     * @param extractTo The unzip destination.
     *
     * @throws IOException Throws IO exception.
     * @throws ZipException Throws Zip exception.
     */
    public static void unzip(@NonNull File zip, @NonNull File extractTo)
            throws SecurityException, IOException {
        ZipFile archive = new ZipFile(zip);
        Enumeration e = archive.entries();

        while (e.hasMoreElements()) {
            ZipEntry entry = (ZipEntry) e.nextElement();
            File file = new File(extractTo, entry.getName());

            if (!file.getCanonicalPath().startsWith(extractTo.getCanonicalPath())) {
                throw new SecurityException("Unsafe unzipping pattern that may " +
                        "lead to a Path Traversal vulnerability.");
            } else {
                if (entry.isDirectory() && !file.exists()) {
                    verifyFile(file);
                } else {
                    if (verifyFile(file.getParentFile())) {
                        InputStream in = archive.getInputStream(entry);
                        BufferedOutputStream out =
                                new BufferedOutputStream(new FileOutputStream(file));

                        byte[] buffer = new byte[8192];
                        int read;

                        while (-1 != (read = in.read(buffer))) {
                            out.write(buffer, 0, read);
                        }

                        in.close();
                        out.close();
                    }
                }
            }
        }
    }

    /**
     * Returns uri from the file. 
     * <p>It will automatically use the @link FileProvider} on API 24 and above devices.
     *
     * @param context The context to get the file provider.
     * @param file The file to get the uri.
     *
     * @return The uri from the file.
     *
     * @see Uri
     */
    public static @Nullable Uri getUriFromFile(@NonNull Context context, @Nullable File file) {
        if (file == null) {
            return null;
        }

        if (DynamicSdkUtils.is23()) {
            return FileProvider.getUriForFile(context.getApplicationContext(),
                    context.getPackageName() + FILE_PROVIDER, file);
        } else {
            return (Uri.fromFile(file));
        }
    }

    /**
     * Returns file name from the uri.
     *
     * @param context The context to get content resolver.
     * @param uri The uri to get the file name.
     *
     * @return The file name from the uri.
     *
     * @see Context#getContentResolver()
     */
    public static @Nullable String getFileNameFromUri(
            @NonNull Context context, @Nullable Uri uri) {
        if (uri == null) {
            return null;
        }

        String fileName = null;
        Cursor cursor = null;

        if (uri.toString().contains(URI_MATCHER_CONTENT)) {
            try {
                cursor = context.getContentResolver().query(uri, null,
                        null, null, null);

                if (cursor != null) {
                    int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
                    cursor.moveToFirst();
                    fileName = cursor.getString(nameIndex);
                }
            } catch (Exception ignored) {
            } finally {
                if (cursor != null) {
                    cursor.close();
                }
            }
        } else if (uri.toString().contains(URI_MATCHER_FILE)) {
            try {
                fileName = new File(new URI(uri.toString())).getName();
            } catch (Exception ignored) {
            }
        }

        return fileName;
    }

    /**
     * Writes a file from the source to destination.
     *
     * @param source The source file.
     * @param destination The destination file.
     * @param outputFileName The output files name.
     *
     * @return {@code true} if the file has been written successfully.
     */
    public static boolean writeToFile(@NonNull File source, 
            @NonNull File destination, @NonNull String outputFileName) {
        boolean success = false;

        try {
            if (DynamicFileUtils.verifyFile(destination)) {
                FileInputStream input = new FileInputStream(source);
                OutputStream output = new FileOutputStream(destination
                        + File.separator + outputFileName);
                byte[] buffer = new byte[1024];
                int length;

                while ((length = input.read(buffer)) > 0) {
                    output.write(buffer, 0, length);
                }

                output.flush();
                output.close();
                input.close();

                success = true;
            }
        } catch (Exception ignored) {
        }

        return success;
    }

    /**
     * Writes a file uri from the source to destination.
     *
     * @param context The context to get content resolver.
     * @param sourceUri The source file uri.
     * @param destinationUri The destination file uri.
     *
     * @return {@code true} if the file has been written successfully.
     */
    public static boolean writeToFile(@NonNull Context context,
            @Nullable Uri sourceUri, @Nullable Uri destinationUri) {
        if (sourceUri == null || destinationUri == null) {
            return false;
        }

        boolean success = false;

        try {
            InputStream input = context.getContentResolver().openInputStream(sourceUri);
            ParcelFileDescriptor pfdDestination = context.getContentResolver().
                    openFileDescriptor(destinationUri, "w");

            if (input != null && pfdDestination != null) {
                OutputStream output = new FileOutputStream(pfdDestination.getFileDescriptor());
                byte[] buffer = new byte[1024];
                int length;

                while ((length = input.read(buffer)) > 0) {
                    output.write(buffer, 0, length);
                }

                output.flush();
                output.close();
                input.close();
                pfdDestination.close();

                success = true;
            }
        } catch (Exception ignored) {
        }

        return success;
    }

    /**
     * Writes a string data to file uri from the source to destination.
     *
     * @param context The context to get content resolver.
     * @param data The string data to be written.
     * @param sourceUri The source file uri.
     * @param destinationUri The destination file uri.
     *
     * @return {@code true} if the file has been written successfully.
     */
    public static boolean writeStringToFile(@NonNull Context context,
            @Nullable String data, @Nullable Uri sourceUri, @Nullable Uri destinationUri) {
        if (sourceUri == null) {
            return false;
        }

        boolean success = false;

        try {
            ParcelFileDescriptor pfdDestination = context.getContentResolver().
                    openFileDescriptor(sourceUri, "w");

            if (pfdDestination != null) {
                OutputStream output = new FileOutputStream(pfdDestination.getFileDescriptor());

                if (data != null) {
                    output.write(data.getBytes());
                }

                output.flush();
                output.close();
                pfdDestination.close();
            }

            if (destinationUri != null) {
                success = writeToFile(context, sourceUri, destinationUri);
            } else {
                success = true;
            }
        } catch (Exception ignored) {
        }

        return success;
    }

    /**
     * Writes a string data to file uri.
     *
     * @param context The context to get content resolver.
     * @param data The string data to be written.
     * @param sourceUri The source file uri.
     *
     * @return {@code true} if the file has been written successfully.
     */
    public static boolean writeStringToFile(@NonNull Context context,
            @Nullable String data, @Nullable Uri sourceUri) {
        return writeStringToFile(context, data, sourceUri, null);
    }

    /**
     * Reads a string data from the file uri.
     *
     * @param context The context to get content resolver.
     * @param fileUri The source file uri.
     *
     * @return The string data after reading the file.
     */
    public static @Nullable String readStringFromFile(
            @NonNull Context context, @NonNull Uri fileUri) {
        try {
            InputStream input = context.getContentResolver().openInputStream(fileUri);

            if (input != null) {
                InputStreamReader inputReader = new InputStreamReader(input);
                BufferedReader bufferedReader = new BufferedReader(inputReader);
                StringBuilder stringBuilder = new StringBuilder();

                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    stringBuilder.append(line);
                }

                input.close();
                inputReader.close();
                bufferedReader.close();

                return stringBuilder.toString();
            }
        } catch (Exception ignored) {
        }

        return null;
    }

    /**
     * Save and returns uri from the bitmap.
     * <p>It will automatically use the @link FileProvider} on API 24 and above devices.
     *
     * <p<p>It requires {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permission on
     * pre KitKat ({@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} or below) devices.
     *
     * @param context The context to get the file provider.
     * @param bitmap The bitmap to get the uri.
     *
     * @return The uri from the bitmap.
     *
     * @see Uri
     */
    public static @Nullable Uri getBitmapUri(@NonNull Context context,
            @Nullable Bitmap bitmap, @NonNull String name) {
        Uri bitmapUri = null;

        if (bitmap != null) {
            try {
                File picturesDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
                if (picturesDir != null) {
                    File storagePath = new File(picturesDir.getPath(), name);
                    String image = storagePath + File.separator + name + ".png";
                    storagePath.mkdirs();

                    FileOutputStream out = new FileOutputStream(image);
                    bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
                    out.close();
                    bitmapUri = getUriFromFile(context, new File(image));
                }
            } catch (Exception ignored) {
            }
        }

        return bitmapUri;
    }

    /**
     * Checks whether the extension is valid for a path.
     *
     * @param path The path string to get the extension.
     * @param extension The extension to be validated.
     *
     * @return {@code true} if the extension is valid for the path.
     */
    public static boolean isValidExtension(@Nullable String path, @Nullable String extension) {
        if (path == null || extension == null) {
            return false;
        }

        return extension.equals("." + getExtension(path));
    }

    /**
     * Checks whether the mime type is valid for an intent data.
     *
     * @param context The context to match the uri mime type.
     * @param intent The intent to get the data.
     * @param mimeType The mime type to be validated.
     * @param extension The optional extension to be validated if mime type is invalid.
     *
     * @return {@code true} if the mime type is valid for the intent data.
     */
    public static boolean isValidMimeType(@Nullable Context context,
            @Nullable Intent intent, @NonNull String mimeType, @Nullable String extension) {
        if (intent == null || intent.getAction() == null) {
            return false;
        }

        boolean validMime = mimeType.equals(intent.getType());

        if (!validMime) {
            if (intent.getParcelableExtra(Intent.EXTRA_STREAM) != null) {
                validMime = isValidMimeType(context,
                        (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM),
                        ADU_MIME_OCTET_STREAM, extension)
                        && isValidExtension(getFileNameFromUri(context,
                        (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM)), extension);
            } else {
                validMime = isValidMimeType(context, intent.getData(),
                        ADU_MIME_OCTET_STREAM, extension) && isValidExtension(
                                getFileNameFromUri(context, intent.getData()), extension);
            }
        }

        return validMime;
    }

    /**
     * Checks whether the mime type is valid for a uri.
     *
     * @param context The context to get the content resolver.
     * @param uri The uri to get the type.
     * @param mimeType The mime type to be validated.
     * @param extension The optional extension to be validated if mime type is invalid.
     *
     * @return {@code true} if the mime type is valid for the intent data.
     */
    public static boolean isValidMimeType(@Nullable Context context,
            @Nullable Uri uri, @NonNull String mimeType, @Nullable String extension) {
        if (context == null || uri == null) {
            return false;
        }

        boolean validMime;

        String type = context.getApplicationContext().getContentResolver().getType(uri);
        validMime = mimeType.equals(type);

        if (type == null) {
            validMime = true;
        }

        if (!validMime) {
            validMime = ADU_MIME_OCTET_STREAM.equals(type)
                    && isValidExtension(getFileNameFromUri(context, uri), extension);
        }

        return validMime;
    }

    /**
     * Checks whether the mime type is valid for a file.
     *
     * @param context The context to get the content resolver.
     * @param file The file to get the uri.
     * @param mimeType The mime type to be validated.
     * @param extension The optional extension to be validated if mime type is invalid.
     *
     * @return {@code true} if the mime type is valid for the intent data.
     */
    public static boolean isValidMimeType(@NonNull Context context,
            @Nullable File file, @NonNull String mimeType, @Nullable String extension) {
        return isValidMimeType(context, getUriFromFile(context, file), mimeType, extension);
    }

    /**
     * Share file according to the mime type.
     *
     * @param activity The activity to create the intent chooser.
     * @param title The title for the intent chooser.
     * @param subject The subject for the intent chooser.
     * @param file The file to be shared.
     * @param mimeType The mime type of the file.
     */
    public static void shareFile(@NonNull Activity activity, @Nullable String title,
            @Nullable String subject, @NonNull File file, @NonNull String mimeType) {
        Intent shareBackup = ShareCompat.IntentBuilder
                .from(activity)
                .setType(mimeType)
                .setSubject(subject != null ? subject : title)
                .setStream(getUriFromFile(activity, file))
                .setChooserTitle(title)
                .createChooserIntent()
                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

        activity.startActivity(shareBackup);
    }

    /**
     * Share multiple files.
     *
     * @param activity The activity to create the intent chooser.
     * @param title The title for the intent chooser.
     * @param subject The subject for the intent chooser.
     * @param uris The content uris to be shared.
     * @param mimeType The mime type of the file.
     */
    public static void shareFiles(@NonNull Activity activity, @Nullable String title,
            @Nullable String subject, @NonNull Uri[] uris, @Nullable String mimeType) {
        ShareCompat.IntentBuilder intentBuilder =
                ShareCompat.IntentBuilder
                        .from(activity)
                        .setSubject(subject != null ? subject : title)
                        .setType(mimeType != null ? mimeType : "*/*")
                        .setChooserTitle(title);

        for (Uri uri : uris) {
            intentBuilder.addStream(uri);
        }

        Intent shareBackup = intentBuilder.createChooserIntent()
                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

        activity.startActivity(shareBackup);
    }

    /**
     * Returns an intent to request a storage location for the supplied file.
     *
     * @param context The context to get the file uri.
     * @param file The file to request the storage location.
     * @param mimeType The mime type of the file.
     *
     * @return The intent to request a storage location for the supplied file.
     */
    public static Intent getSaveToFileIntent(@NonNull Context context,
            @NonNull File file, @NonNull String mimeType) {
        Uri uri = getUriFromFile(context, file);
        Intent intent;

        if (DynamicSdkUtils.is19()) {
            intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
        } else {
            intent = new Intent(Intent.ACTION_PICK);
        }

        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.putExtra(Intent.EXTRA_TITLE, file.getName());
        intent.setType(mimeType);
        intent.putExtra(Intent.EXTRA_STREAM, uri);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);

        return intent;
    }

    /**
     * Returns an intent to select a file according to the mime type.
     *
     * @param mimeType The mime type for the file.
     *
     * @return The intent intent to select a file according to the mime type.
     */
    public static Intent getFileSelectIntent(@NonNull String mimeType) {
        Intent intent;

        if (DynamicSdkUtils.is19()) {
            intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
            intent.setType("*/*");
        } else {
            intent = new Intent(Intent.ACTION_GET_CONTENT);
            intent.setType(mimeType);
        }

        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);

        return intent;
    }
}