package com.fekracomputers.islamiclibrary.utility;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.preference.PreferenceManager;

import com.fekracomputers.islamiclibrary.BuildConfig;
import com.fekracomputers.islamiclibrary.R;
import com.fekracomputers.islamiclibrary.download.model.DownloadFileConstants;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;

import timber.log.Timber;

import static com.fekracomputers.islamiclibrary.databases.BooksInformationDbHelper.DATABASE_FULL_NAME;
import static com.fekracomputers.islamiclibrary.download.model.DownloadFileConstants.PREF_SDCARDPERMESSION_DIALOG_DISPLAYED;

/**
 * <pre>
 * Based on:
 * - https://github.com/quran/quran_android/blob/master/app/src/main/java/com/quran/labs/androidquran/util/StorageUtils.java
 * - https://github.com/quran/quran_android/blob/master/app/src/main/java/com/quran/labs/androidquran/util/QuranFileUtils.java
 * - http://sapienmobile.com/?p=204
 * - http://stackoverflow.com/a/15612964
 * - http://renzhi.ca/2012/02/03/how-to-list-all-sd-cards-on-android/
 * </pre>
 */
public class StorageUtils {

    public static String getIslamicLibraryShamelaBooksDir(@NonNull Context context) {
        String base = getIslamicLibraryBaseDirectory(context);
        return base == null ? null : base + File.separator + DownloadFileConstants.SHAMELA_BOOKS_DIR;

    }

    public static String getIslamicLibraryUserBooksDir(Context context) {
        String base = getIslamicLibraryBaseDirectory(context);
        return base == null ? null : base + File.separator + DownloadFileConstants.USER_BOOKS_DIR;
    }


    @Nullable
    public static String getIslamicLibraryBaseDirectory(@NonNull Context context) {
        String basePath = getAppCustomLocation(context);

        if (!isSDCardMounted()) {
            // if our best guess suggests that we won't have access to the data due to the sdcard not
            // being mounted, then set the base path to null for now.
            if (basePath == null ||
                    basePath.equals(Environment.getExternalStorageDirectory().getAbsolutePath()) ||
                    (basePath.contains(BuildConfig.APPLICATION_ID) && context.getExternalFilesDir(null) == null)) {
                basePath = null;
            }
        }

        if (basePath != null) {
            if (!basePath.endsWith(File.separator)) {
                basePath += File.separator;
            }
            return basePath + DownloadFileConstants.ISLAMIC_LIBRARY_BASE_DIRECTORY;
        }
        return null;
    }

    /**
     * @return string representing the path to the root custom external storage
     */
    @Nullable
    public static String getAppCustomLocation(@NonNull Context context) {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        return sharedPreferences.getString(DownloadFileConstants.PREF_APP_LOCATION,
                Environment.getExternalStorageDirectory().getAbsolutePath());
    }

    private static boolean isSDCardMounted() {
        String state = Environment.getExternalStorageState();
        return state.equals(Environment.MEDIA_MOUNTED);
    }


    public static boolean haveWriteExternalStoragePermission(@NonNull Context context) {
        return Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
                ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
                        PackageManager.PERMISSION_GRANTED;
    }

    public static void setAppCustomLocation(String location, @NonNull Context context) {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString(DownloadFileConstants.PREF_APP_LOCATION, location);
        editor.commit();

    }

    public static void setSdcardPermissionsDialogPresented(@NonNull Context context) {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putBoolean(PREF_SDCARDPERMESSION_DIALOG_DISPLAYED, true);
        editor.commit();

    }

    public static boolean makeIslamicLibraryShamelaDirectory(@NonNull Context context) {
        String path = getIslamicLibraryShamelaBooksDir(context);
        if (path == null) {
            return false;
        }
        File directory = new File(path);
        return (directory.exists() && directory.isDirectory()) || directory.mkdirs();
    }

    public static boolean isOldDirectoriesExists(@NonNull Context context) {
        String oldPath = getIslamicLibraryBaseDirectory(context) + File.separator +
                DATABASE_FULL_NAME;
        return new File(oldPath).exists();

    }

    public static boolean moveAppFiles(@NonNull Context context, String newLocation, boolean automatic) {
        if (getAppCustomLocation(context).equals(newLocation)) {
            return true;
        }
        final String baseDir = getIslamicLibraryBaseDirectory(context);
        if (baseDir == null) {
            return false;
        }
        File currentDirectory = new File(baseDir);
        File newDirectory = new File(newLocation, DownloadFileConstants.ISLAMIC_LIBRARY_BASE_DIRECTORY);
        if (!currentDirectory.exists()) {
            // No files to copy, so change the app directory directly
            return true;
        } else if (newDirectory.exists() || newDirectory.mkdirs()) {
            if (automatic) {
                try {
                    copyFileOrDirectory(currentDirectory, newDirectory);
                    deleteFileOrDirectory(currentDirectory);
                    return true;
                } catch (IOException e) {
                    Timber.e(e, "error moving app files");
                }
            } else {
                return true;
            }
        }
        return false;
    }

    private static void deleteFileOrDirectory(@NonNull File file) {
        if (file.isDirectory()) {
            File[] subFiles = file.listFiles();
            // subFiles is null on some devices, despite this being a directory
            int length = subFiles == null ? 0 : subFiles.length;
            for (int i = 0; i < length; i++) {
                File sf = subFiles[i];
                if (sf.isFile()) {
                    if (!sf.delete()) {
                        Timber.e("Error deleting %s", sf.getPath());
                    }
                } else {
                    deleteFileOrDirectory(sf);
                }
            }
        }
        if (!file.delete()) {
            Timber.e("Error deleting %s", file.getPath());
        }
    }

    public static void copyFileOrDirectory(@NonNull File source, @NonNull File destination) throws IOException {
        if (source.isDirectory()) {
            if (!destination.exists() && !destination.mkdirs()) {
                return;
            }
            File[] files = source.listFiles();
            for (File f : files) {
                copyFileOrDirectory(f, new File(destination, f.getName()));
            }
        } else {
            copyFile(source, destination);
        }
    }


    public static void copyFile(@NonNull File source, @NonNull File destination) throws IOException {
        FileInputStream inStream = new FileInputStream(source);
        FileOutputStream outStream = openOutputStream(destination);
        FileChannel inChannel = inStream.getChannel();
        FileChannel outChannel = outStream.getChannel();
        try {
            inChannel.transferTo(0, inChannel.size(), outChannel);
        } finally {
            inStream.close();
            outStream.close();
        }

    }

    /**
     * @return A List of all storage locations available
     */
    @NonNull
    public static List<Storage> getAllStorageLocations(@NonNull Context context) {

    /*
      This first condition is the code moving forward, since the else case is a bunch
      of unsupported hacks.

      For Kitkat and above, we rely on Environment.getExternalFilesDirs to give us a list
      of application writable directories (none of which require WRITE_EXTERNAL_STORAGE on
      Kitkat and above).

      Previously, we only would show anything if there were at least 2 entries. For M,
      some changes were made, such that on M, we even show this if there is only one
      entry.

      Irrespective of whether we require 1 entry (M) or 2 (Kitkat and L), we add an
      additional entry explicitly for the sdcard itself, (the one requiring
      WRITE_EXTERNAL_STORAGE to write).

      Thus, on Kitkat, the user may either:
      a. not see any item (if there's only one entry returned by getExternalFilesDirs, we won't
      show any options since it's the same sdcard and we have the permission and the user can't
      revoke it pre-Kitkat), or
      b. see 3+ items - /sdcard, and then at least 2 external fiels directories.

      on M, the user will always see at least 2 items (the external files dir and the actual
      external storage directory), and potentially more (depending on how many items are returned
      by getExternalFilesDirs).
     */
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            List<Storage> result = new ArrayList<>();
            int limit = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? 1 : 2;
            final File[] mountPoints = ContextCompat.getExternalFilesDirs(context, null);
            if (mountPoints != null && mountPoints.length >= limit) {
                int typeId;
                if (!Environment.isExternalStorageRemovable() || Environment.isExternalStorageEmulated()) {
                    typeId = R.string.prefs_sdcard_internal;
                } else {
                    typeId = R.string.prefs_sdcard_external;
                }

                int number = 1;
                result.add(new Storage(context.getString(typeId, number),
                        Environment.getExternalStorageDirectory().getAbsolutePath(),
                        Build.VERSION.SDK_INT >= Build.VERSION_CODES.M));
                for (File mountPoint : mountPoints) {
                    if (mountPoint != null) {
                        result.add(new Storage(context.getString(typeId, number++),
                                mountPoint.getAbsolutePath()));
                        typeId = R.string.prefs_sdcard_external;
                    }
                }
            }
            return result;
        } else {
            return getLegacyStorageLocations(context);
        }
    }

    /**
     * Attempt to return a list of storage locations pre-Kitkat.
     *
     * @param context the context
     * @return the list of storage locations
     */
    @NonNull
    private static List<Storage> getLegacyStorageLocations(@NonNull Context context) {
        List<String> mounts = readMountsFile();

        // As per http://source.android.com/devices/tech/storage/config.html
        // device-specific vold.fstab file is removed after Android 4.2.2
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            Set<String> volds = readVoldsFile();

            List<String> toRemove = new ArrayList<>();
            for (String mount : mounts) {
                if (!volds.contains(mount)) {
                    toRemove.add(mount);
                }
            }

            mounts.removeAll(toRemove);
        } else {
            Timber.d("Android version: %d, skip reading vold.fstab file", Build.VERSION.SDK_INT);
        }

        Timber.d("mounts list is: %s", mounts);
        return buildMountsList(context, mounts);
    }

    /**
     * Converts a list of mount strings to a list of Storage items
     *
     * @param context the context
     * @param mounts  a list of mount points as strings
     * @return a list of Storage items that can be rendered by the ui
     */
    @NonNull
    private static List<Storage> buildMountsList(@NonNull Context context, @NonNull List<String> mounts) {
        List<Storage> list = new ArrayList<>(mounts.size());

        int externalSdcardsCount = 0;
        if (mounts.size() > 0) {
            // Follow Android SD Cards naming conventions
            if (!Environment.isExternalStorageRemovable() || Environment.isExternalStorageEmulated()) {
                list.add(new Storage(context.getString(R.string.prefs_sdcard_internal),
                        Environment.getExternalStorageDirectory().getAbsolutePath()));
            } else {
                externalSdcardsCount = 1;
                list.add(new Storage(context.getString(R.string.prefs_sdcard_external,
                        externalSdcardsCount), mounts.get(0)));
            }

            // All other mounts rather than the first mount point are considered as External SD Card
            if (mounts.size() > 1) {
                externalSdcardsCount++;
                for (int i = 1/*skip the first item*/; i < mounts.size(); i++) {
                    list.add(new Storage(context.getString(R.string.prefs_sdcard_external,
                            externalSdcardsCount++), mounts.get(i)));
                }
            }
        }

        Timber.d("final storage list is: %s", list);
        return list;
    }

    /**
     * Read /proc/mounts. This is a set of hacks for versions below Kitkat.
     *
     * @return list of mounts based on the mounts file.
     */
    @NonNull
    private static List<String> readMountsFile() {
        String sdcardPath = Environment.getExternalStorageDirectory().getAbsolutePath();
        List<String> mounts = new ArrayList<>();
        mounts.add(sdcardPath);

        Timber.d("reading mounts file begin");
        try {
            File mountFile = new File("/proc/mounts");
            if (mountFile.exists()) {
                Timber.d("mounts file exists");
                Scanner scanner = new Scanner(mountFile);
                while (scanner.hasNext()) {
                    String line = scanner.nextLine();
                    Timber.d("line: %s", line);
                    if (line.startsWith("/dev/block/vold/")) {
                        String[] lineElements = line.split(" ");
                        String element = lineElements[1];
                        Timber.d("mount element is: %s", element);
                        if (!sdcardPath.equals(element)) {
                            mounts.add(element);
                        }
                    } else {
                        Timber.d("skipping mount line: %s", line);
                    }
                }
            } else {
                Timber.d("mounts file doesn't exist");
            }

            Timber.d("reading mounts file end.. list is: %s", mounts);
        } catch (Exception e) {
            Timber.e(e, "Error reading mounts file");
        }
        return mounts;
    }

    /**
     * Reads volume manager daemon file for auto-mounted storage.
     * Read more about it <a href="http://vold.sourceforge.net/">here</a>.
     * <p>
     * Set usage, to safely avoid duplicates, is intentional.
     *
     * @return Set of mount points from `vold.fstab` configuration file
     */
    @NonNull
    private static Set<String> readVoldsFile() {
        Set<String> volds = new HashSet<>();
        volds.add(Environment.getExternalStorageDirectory().getAbsolutePath());

        Timber.d("reading volds file");
        try {
            File voldFile = new File("/system/etc/vold.fstab");
            if (voldFile.exists()) {
                Timber.d("reading volds file begin");
                Scanner scanner = new Scanner(voldFile);
                while (scanner.hasNext()) {
                    String line = scanner.nextLine();
                    Timber.d("line: %s", line);
                    if (line.startsWith("dev_mount")) {
                        String[] lineElements = line.split(" ");
                        String element = lineElements[2];
                        Timber.d("volds element is: %s", element);

                        if (element.contains(":")) {
                            element = element.substring(0, element.indexOf(":"));
                            Timber.d("volds element is: %s", element);
                        }

                        Timber.d("adding volds element to list: %s", element);
                        volds.add(element);
                    } else {
                        Timber.d("skipping volds line: %s", line);
                    }
                }
            } else {
                Timber.d("volds file doesn't exit");
            }
            Timber.d("reading volds file end.. list is: %s", volds);
        } catch (Exception e) {
            Timber.e(e, "Error reading volds file");
        }

        return volds;
    }

    public static int getAppUsedSpace(@NonNull Context context) {
        final String baseDirectory = getIslamicLibraryBaseDirectory(context);
        if (baseDirectory == null) {
            return -1;
        }

        File base = new File(baseDirectory);
        ArrayList<File> files = new ArrayList<>();
        files.add(base);
        long size = 0;
        while (!files.isEmpty()) {
            File f = files.remove(0);
            if (f.isDirectory()) {
                File[] subFiles = f.listFiles();
                if (subFiles != null) {
                    Collections.addAll(files, subFiles);
                }
            } else {
                size += f.length();
            }
        }
        return (int) (size / (long) (1024 * 1024));
    }

    public static boolean didPresentSdcardPermissionsDialog(@NonNull Activity activity) {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
        return sharedPreferences.getBoolean(PREF_SDCARDPERMESSION_DIALOG_DISPLAYED, false);
    }


    public static class Storage {
        private final String label;
        private final String mountPoint;
        private final boolean requiresPermission;

        private int freeSpace;

        Storage(String label, String mountPoint) {
            this(label, mountPoint, false);
        }

        Storage(String label, String mountPoint, boolean requiresPermission) {
            this.label = label;
            this.mountPoint = mountPoint;
            this.requiresPermission = requiresPermission;
            computeSpace();
        }

        private void computeSpace() {
            StatFs stat = new StatFs(mountPoint);
            long bytesAvailable;
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) {
                bytesAvailable = stat.getAvailableBlocksLong() * stat.getBlockSizeLong();
            } else {
                //noinspection deprecation
                bytesAvailable = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize();
            }
            // Convert total bytes to megabytes
            freeSpace = Math.round(bytesAvailable / (1024 * 1024));
        }

        public String getLabel() {
            return label;
        }

        public String getMountPoint() {
            return mountPoint;
        }

        /**
         * @return available free size in Megabytes
         */
        public int getFreeSpace() {
            return freeSpace;
        }

        public boolean doesRequirePermission() {
            return requiresPermission;
        }
    }

    /**
     * Opens a {@link FileInputStream} for the specified file, providing better
     * error messages than simply calling <code>new FileInputStream(file)</code>.
     * <p>
     * At the end of the method either the stream will be successfully opened,
     * or an exception will have been thrown.
     * <p>
     * An exception is thrown if the file does not exist.
     * An exception is thrown if the file object exists but is a directory.
     * An exception is thrown if the file exists but cannot be read.
     *
     * @param file the file to open for input, must not be <code>null</code>
     * @return a new {@link FileInputStream} for the specified file
     * @throws FileNotFoundException if the file does not exist
     * @throws IOException           if the file object is a directory
     * @throws IOException           if the file cannot be read
     * @since Commons IO 1.3
     */
    public static FileInputStream openInputStream(File file) throws IOException {
        if (file.exists()) {
            if (file.isDirectory()) {
                throw new IOException("File '" + file + "' exists but is a directory");
            }
            if (file.canRead() == false) {
                throw new IOException("File '" + file + "' cannot be read");
            }
        } else {
            throw new FileNotFoundException("File '" + file + "' does not exist");
        }
        return new FileInputStream(file);
    }

    /**
     * Opens a {@link FileOutputStream} for the specified file, checking and
     * creating the parent directory if it does not exist.
     * <p>
     * At the end of the method either the stream will be successfully opened,
     * or an exception will have been thrown.
     * <p>
     * The parent directory will be created if it does not exist.
     * The file will be created if it does not exist.
     * An exception is thrown if the file object exists but is a directory.
     * An exception is thrown if the file exists but cannot be written to.
     * An exception is thrown if the parent directory cannot be created.
     *
     * @param file the file to open for output, must not be <code>null</code>
     * @return a new {@link FileOutputStream} for the specified file
     * @throws IOException if the file object is a directory
     * @throws IOException if the file cannot be written to
     * @throws IOException if a parent directory needs creating but that fails
     * @since Commons IO 1.3
     */
    public static FileOutputStream openOutputStream(File file) throws IOException {
        if (file.exists()) {
            if (file.isDirectory()) {
                throw new IOException("File '" + file + "' exists but is a directory");
            }
            if (!file.canWrite()) {
                throw new IOException("File '" + file + "' cannot be written to");
            }
        } else {
            File parent = file.getParentFile();
            if (parent != null && !parent.exists()) {
                if (!parent.mkdirs()) {
                    throw new IOException("File '" + file + "' could not be created");
                }
            }
        }
        return new FileOutputStream(file);
    }

}