package com.datarecovery.master.utils.xfile;

import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.CancellationSignal;
import android.os.storage.StorageVolume;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;

import com.atmob.common.logging.AtmobLog;
import com.atmob.common.thread.ThreadPoolUtil;
import com.datarecovery.master.utils.FileUtil;

import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Stack;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class XFileSearch {

    private static final int SEARCH_MODE_DFS = 0;
    private static final int SEARCH_MODE_BFS = 1;
    private static final int SEARCH_MODE = SEARCH_MODE_BFS;

    /**
     * 搜索外部存储
     * <p>
     * Android Q以下：直接通过文件路径访问,需要权限{@link android.Manifest.permission#READ_EXTERNAL_STORAGE}
     * & {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE}
     * <p>
     * Android Q及以上：除了应用私有目录，可以通过文件路径访问，需要权限{@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE}
     * 应用私有目录只能通过SAF访问，需要用户手动授权可以访问的目录
     */
    public static List<XFile> searchExternalStorage(Context context, List<String[]> specifyScanPaths, FileFilter fileFilter) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            return searchExternalStorageOld(context, specifyScanPaths, fileFilter);
        } else {
            return searchExternalStorageNew(context, specifyScanPaths, fileFilter);
        }
    }

    /**
     * 异步搜索外部存储, 回调在子线程
     * <p>
     * 使用线程池执行搜索任务{@link ThreadPoolUtil#execute(Runnable)}
     */
    public static CancellationSignal searchExternalStorageAsync(Context context, FileFilter fileFilter, FileSearchCallback callback) {
        CancellationSignal cancellationSignal = new CancellationSignal();
        ThreadPoolUtil.getInstance().execute(() -> {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
                searchExternalStorageOldAsync(context, null, fileFilter, callback, cancellationSignal);
            } else {
                searchExternalStorageNewAsync(context, null, null, fileFilter, callback, cancellationSignal);
            }
        });
        return cancellationSignal;
    }


    public static CancellationSignal searchPreExternalStorageAsync(Context context, List<String[]> filePreScanDirectory, FileFilter fileFilter, FileSearchCallback callback) {
        CancellationSignal cancellationSignal = new CancellationSignal();
        ThreadPoolUtil.getInstance().execute(() -> {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
                searchExternalStorageOldAsync(context, null, fileFilter, callback, cancellationSignal);
            } else {
                searchExternalStorageNewAsync(context, filePreScanDirectory, null, fileFilter, callback, cancellationSignal);
            }
        });
        return cancellationSignal;
    }

    public static CancellationSignal searchSpecifyExternalStorageAsync(Context context, List<String[]> specifyScanPaths, FileFilter fileFilter, FileSearchCallback callback) {
        CancellationSignal cancellationSignal = new CancellationSignal();
        ThreadPoolUtil.getInstance().execute(() -> {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
                searchExternalStorageOldAsync(context, specifyScanPaths, fileFilter, callback, cancellationSignal);
            } else {
                searchExternalStorageNewAsync(context, null, specifyScanPaths, fileFilter, callback, cancellationSignal);
            }
        });
        return cancellationSignal;
    }


    private static void searchExternalStorageNewAsync(Context context, List<String[]> filePreScanDirectory, List<String[]> specifyScanPaths, FileFilter fileFilter,
                                                      FileSearchCallback callback,
                                                      CancellationSignal cancellationSignal) {
        final int maxThreadCount = 5;
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        List<XFile> startFiles = getExternalStorageFilesNew(context, filePreScanDirectory, specifyScanPaths, fileFilter);
        ArrayList<XFile> tasks = new ArrayList<>();
        for (XFile startFile : startFiles) {
            try {
                XFile[] xFiles = startFile.listFiles();
                if (xFiles != null) {
                    tasks.addAll(Arrays.asList(xFiles));
                }
            } catch (Exception ignore) {
            }
        }
        HashMap<Integer, List<XFile>> splitTask = new HashMap<>();
        for (int i = 0; i < tasks.size(); i++) {
            int index = (int) (Math.random() * maxThreadCount);
            List<XFile> xFiles = splitTask.get(index);
            if (xFiles == null) {
                xFiles = new ArrayList<>();
                splitTask.put(index, xFiles);
            }
            xFiles.add(tasks.get(i));
        }
        if (callback != null) {
            callback.onStart();
        }
        int[] finishCount = new int[maxThreadCount];
        int currentIndex = 0;
        for (List<XFile> value : splitTask.values()) {
            final int finalCurrentIndex = currentIndex++;
            ThreadPoolUtil.getInstance().execute(() -> searchInternalAsync(value, fileFilter, new FileSearchCallback() {
                @Override
                public void onStart() {
                }

                @Override
                public void onEachFile(XFile file) {
                    if (callback != null) {
                        callback.onEachFile(file);
                    }
                }

                @Override
                public void onFinish() {
                    finishCount[finalCurrentIndex] = 1;
                    lock.lock();
                    try {
                        for (int j : finishCount) {
                            if (j == 0) {
                                return;
                            }
                        }
                        condition.signal();
                    } finally {
                        lock.unlock();
                    }
                }
            }, cancellationSignal));
        }
        lock.lock();
        try {
            for (int j : finishCount) {
                if (j == 0) {
                    condition.await();
                    break;
                }
            }
            if (callback != null) {
                callback.onFinish();
            }
        } catch (InterruptedException ignore) {
        } finally {
            lock.unlock();
        }
    }

    private static void searchExternalStorageOldAsync(Context context, List<String[]> specifyScanPaths, FileFilter fileFilter,
                                                      FileSearchCallback callback,
                                                      CancellationSignal cancellationSignal) {
        List<XFile> startFiles = getExternalStorageFilesOld(context, specifyScanPaths);
        searchInternalAsync(startFiles, fileFilter, callback, cancellationSignal);
    }

    private static List<XFile> searchExternalStorageNew(Context context, List<String[]> specifyScanPaths, FileFilter fileFilter) {
        List<XFile> startFiles = getExternalStorageFilesNew(context, null, specifyScanPaths, fileFilter);
        return searchInternal(startFiles, fileFilter);
    }

    private static List<XFile> searchExternalStorageOld(Context context, List<String[]> specifyScanPaths, FileFilter fileFilter) {
        List<XFile> startFiles = getExternalStorageFilesOld(context, specifyScanPaths);
        return searchInternal(startFiles, fileFilter);
    }

    public static List<XFile> searchAndroidData(Context context, FileFilter fileFilter) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            return searchAndroidDataOld(context, fileFilter);
        } else {
            return searchAndroidDataNew(context, fileFilter);
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.Q)
    private static List<XFile> searchAndroidDataNew(Context context, FileFilter fileFilter) {
        List<StorageVolume> readableStorageVolumes = XStorageManager.getReadableStorageVolumes(context);
        List<XFile> startFiles = new ArrayList<>();
        for (StorageVolume storageVolume : readableStorageVolumes) {
            Uri treeUri = StorageVolumeUtil.getTreeUri(storageVolume);
            XSAFFile xFile = new XSAFFile(context, treeUri, Arrays.asList("Android", "data"));
            try {
                if (xFile.exists()) {
                    startFiles.add(xFile);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return searchInternal(startFiles, fileFilter);
    }

    private static List<XFile> searchAndroidDataOld(Context context, FileFilter fileFilter) {
        List<StorageVolume> readableStorageVolumes = XStorageManager.getReadableStorageVolumes(context);
        List<XFile> startFiles = new ArrayList<>();
        for (StorageVolume storageVolume : readableStorageVolumes) {
            File directory = StorageVolumeUtil.getDirectory(storageVolume);
            if (directory == null) {
                continue;
            }
            XFile androidData = new XPathFile(context, directory, "Android/data");
            try {
                if (androidData.exists()) {
                    startFiles.add(androidData);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return searchInternal(startFiles, fileFilter);
    }

    @NonNull
    private static List<XFile> getExternalStorageFilesNew(Context context, List<String[]> filePreScanDirectory, List<String[]> specifyScanPaths, FileFilter fileFilter) {
        List<StorageVolume> readableStorageVolumes = XStorageManager.getReadableStorageVolumes(context);
        List<XFile> startFiles = new ArrayList<>();
        for (StorageVolume storageVolume : readableStorageVolumes) {
            Uri treeUri = StorageVolumeUtil.getTreeUri(storageVolume);
            File directory = StorageVolumeUtil.getDirectory(storageVolume);
            if (specifyScanPaths != null && !specifyScanPaths.isEmpty()) {
                for (String[] scanPath : specifyScanPaths) {
                    String filePath = FileUtil.getFilePath(scanPath);
                    if (filePath.contains("Android%2Fdata")) {
                        XSAFFile xsafFile = new XSAFFile(context, treeUri, scanPath);
                        try {
                            if (xsafFile.exists()) {
                                startFiles.add(xsafFile);
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    } else {
                        if (directory != null) {
                            XPathFile xPathFile = new XPathFile(context, directory, filePath);
                            try {
                                if (xPathFile.exists()) {
                                    startFiles.add(xPathFile);
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            } else if (filePreScanDirectory != null && !filePreScanDirectory.isEmpty()) {
                List<XFile> normalFiles = new ArrayList<>();
                List<XFile> androidDataFiles = new ArrayList<>();
                List<XFile> allFiles = new ArrayList<>();

                for (String[] scanPath : filePreScanDirectory) {
                    String filePath = FileUtil.getFilePath(scanPath);
                    if (filePath.contains("Android%2Fdata")) {
                        XSAFFile xsafFile = new XSAFFile(context, treeUri, scanPath);
                        try {
                            if (xsafFile.exists()) {
                                allFiles.add(xsafFile);
                                androidDataFiles.add(xsafFile);
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    } else {
                        if (directory != null) {
                            XPathFile xPathFile = new XPathFile(context, directory, filePath);
                            try {
                                if (xPathFile.exists()) {
                                    allFiles.add(xPathFile);
                                    normalFiles.add(xPathFile);
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
                if (directory != null) {
                    XPathFile androidEx = new XPathFile(context, directory);
                    XFile androidXSA = null;
                    XSAFFile androidDataXSA = new XSAFFile(context, treeUri, new String[]{"Android", "data"});
                    if (androidDataXSA.exists()) {
                        XFile[] xFiles = androidDataXSA.listFiles();
                        if (xFiles != null) {
                            for (XFile x : xFiles) {
                                XFile tag = null;
                                for (XFile next : androidDataFiles) {
                                    try {
                                        if (x.getPath().equals(next.getPath())) {
                                            tag = next;
                                            break;
                                        }
                                    } catch (Exception ignore) {
                                    }
                                }
                                if (tag == null) {
                                    allFiles.add(x);
                                } else {
                                    androidDataFiles.remove(tag);
                                }
                            }
                        }
                    }
                    if (androidEx.exists()) {
                        XFile[] xFiles = androidEx.listFiles();
                        if (xFiles != null) {
                            for (XFile x : xFiles) {
                                try {
                                    if ("Android".equals(x.getName())) {
                                        androidXSA = x;
                                        continue;
                                    }
                                } catch (Exception ignore) {
                                }
                                XFile tag = null;
                                for (XFile next : normalFiles) {
                                    try {
                                        if (x.getPath().equals(next.getPath())) {
                                            tag = next;
                                            break;
                                        }
                                    } catch (Exception ignore) {
                                    }
                                }
                                if (tag == null) {
                                    allFiles.add(x);
                                } else {
                                    normalFiles.remove(tag);
                                }
                            }
                        }
                    }
                    try {
                        if (androidXSA != null && androidXSA.exists()) {
                            XFile[] xFiles = androidXSA.listFiles();
                            if (xFiles != null) {
                                for (XFile xFile : xFiles) {
                                    try {
                                        if (xFile.getPath().contains("Android%2Fdata") || xFile.getPath().contains("Android/data")) {
                                            continue;
                                        }
                                        allFiles.add(xFile);
                                    } catch (Exception ignore) {
                                    }
                                }
                            }
                        }
                    } catch (Exception ignore) {
                    }
                }
                startFiles.addAll(allFiles);
            } else {
                XSAFFile xsafFile = new XSAFFile(context, treeUri, Arrays.asList("Android", "data"));
                try {
                    if (xsafFile.exists()) {
                        startFiles.add(xsafFile);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (directory != null) {
                    XPathFile xPathFile = new XPathFile(context, directory);
                    try {
                        if (xPathFile.exists()) {
                            startFiles.add(xPathFile);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return startFiles;
    }

    @NonNull
    private static List<XFile> getExternalStorageFilesOld(Context context, List<String[]> specifyScanPaths) {
        List<StorageVolume> readableStorageVolumes = XStorageManager.getReadableStorageVolumes(context);
        List<XFile> startFiles = new ArrayList<>();
        for (StorageVolume storageVolume : readableStorageVolumes) {
            File directory = StorageVolumeUtil.getDirectory(storageVolume);
            if (directory == null) {
                continue;
            }
            if (specifyScanPaths != null && !specifyScanPaths.isEmpty()) {
                for (String[] searchPath : specifyScanPaths) {
                    try {
                        File file = new File(directory, FileUtil.getFilePath(searchPath));
                        if (file.exists()) {
                            startFiles.add(new XPathFile(context, file));
                        }
                    } catch (Exception ignore) {

                    }
                }
            } else {
                XFile androidData = new XPathFile(context, directory);
                try {
                    if (androidData.exists()) {
                        startFiles.add(androidData);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return startFiles;
    }

    private static List<XFile> searchInternal(List<XFile> startFiles, FileFilter fileFilter) {
        return SEARCH_MODE == SEARCH_MODE_DFS ? dfsSearch(startFiles, fileFilter) : bfsSearch(startFiles, fileFilter);
    }

    private static void searchInternalAsync(List<XFile> startFiles, FileFilter fileFilter,
                                            FileSearchCallback callback, CancellationSignal cancellationSignal) {
        if (SEARCH_MODE == SEARCH_MODE_DFS) {
            dfsSearchAsync(startFiles, fileFilter, callback, cancellationSignal);
        } else {
            bfsSearchAsync(startFiles, fileFilter, callback, cancellationSignal);
        }
    }

    /**
     * 深度优先搜索
     */
    private static List<XFile> dfsSearch(List<XFile> startFiles, FileFilter fileFilter) {
        Stack<XFile> fileStock = new Stack<>();
        fileStock.addAll(startFiles);
        ArrayList<XFile> result = new ArrayList<>();
        while (!fileStock.isEmpty()) {
            XFile file = fileStock.pop();
            if (file == null) {
                continue;
            }
            try {
                XFile[] files = file.listFiles();
                if (files == null) {
                    continue;
                }
                for (XFile childFile : files) {
                    if (childFile == null) {
                        continue;
                    }
                    if (childFile.isDirectory()) {
                        if (fileFilter == null) {
                            fileStock.push(childFile);
                            continue;
                        }
                        if (fileFilter.filterDirectory(childFile)) {
                            continue;
                        }
                        if (fileFilter.acceptDirectory(childFile)) {
                            result.add(childFile);
                        } else {
                            fileStock.push(childFile);
                        }
                    } else if (fileFilter == null || fileFilter.acceptFile(childFile)) {
                        result.add(childFile);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    /**
     * 深度优先搜索
     */
    private static void dfsSearchAsync(List<XFile> startFiles, FileFilter fileFilter,
                                       FileSearchCallback callback, CancellationSignal cancellationSignal) {
        if (fileFilter == null) {
            throw new RuntimeException("FileFilter is null");
        }
        Stack<XFile> fileStock = new Stack<>();
        fileStock.addAll(startFiles);
        if (callback != null) {
            callback.onStart();
        }
        while (!fileStock.isEmpty()) {
            if (cancellationSignal.isCanceled()) {
                break;
            }
            XFile file = fileStock.pop();
            if (file == null) {
                continue;
            }
            try {
                if (file.isDirectory()) {
                    if (fileFilter.filterDirectory(file)) {
                        continue;
                    }
                    if (fileFilter.acceptDirectory(file)) {
                        if (callback != null) {
                            callback.onEachFile(file);
                        }
                        continue;
                    }
                    XFile[] files = file.listFiles();
                    if (files == null) {
                        continue;
                    }
                    for (XFile childFile : files) {
                        if (childFile == null) {
                            continue;
                        }
                        fileStock.push(childFile);
                    }
                } else if (fileFilter.acceptFile(file)) {
                    if (callback != null) {
                        callback.onEachFile(file);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (callback != null) {
            callback.onFinish();
        }
    }

    /**
     * 广度优先搜索
     */
    private static List<XFile> bfsSearch(List<XFile> startFiles, FileFilter fileFilter) {
        Queue<XFile> fileQueue = new LinkedList<>(startFiles);
        ArrayList<XFile> result = new ArrayList<>();
        while (!fileQueue.isEmpty()) {
            XFile file = fileQueue.poll();
            if (file == null) {
                continue;
            }
            try {
                XFile[] files = file.listFiles();
                if (files == null) {
                    continue;
                }
                for (XFile childFile : files) {
                    if (childFile == null) {
                        continue;
                    }
                    if (childFile.isDirectory()) {
                        if (fileFilter == null) {
                            fileQueue.offer(childFile);
                            continue;
                        }
                        if (fileFilter.filterDirectory(childFile)) {
                            continue;
                        }
                        if (fileFilter.acceptDirectory(childFile)) {
                            result.add(childFile);
                        } else {
                            fileQueue.offer(childFile);
                        }
                    } else if (fileFilter == null || fileFilter.acceptFile(childFile)) {
                        result.add(childFile);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    /**
     * 广度优先搜索
     */
    private static void bfsSearchAsync(List<XFile> startFiles, FileFilter fileFilter,
                                       FileSearchCallback callback, CancellationSignal cancellationSignal) {
        if (fileFilter == null) {
            throw new RuntimeException("FileFilter is null");
        }
        Queue<XFile> fileQueue = new LinkedList<>(startFiles);
        if (callback != null) {
            callback.onStart();
        }
        while (!fileQueue.isEmpty()) {
            if (cancellationSignal.isCanceled()) {
                break;
            }
            XFile file = fileQueue.poll();
            if (file == null) {
                continue;
            }
            try {
                if (file.isDirectory()) {
                    if (fileFilter.filterDirectory(file)) {
                        continue;
                    }
                    if (fileFilter.acceptDirectory(file)) {
                        if (callback != null) {
                            callback.onEachFile(file);
                        }
                        continue;
                    }
                    XFile[] files = file.listFiles();
                    if (files != null) {
                        for (XFile childFile : files) {
                            if (childFile == null) {
                                continue;
                            }
                            fileQueue.offer(childFile);
                        }
                    }
                } else if (fileFilter.acceptFile(file)) {
                    if (callback != null) {
                        callback.onEachFile(file);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (callback != null) {
            callback.onFinish();
        }
    }


    public interface FileFilter {

        boolean acceptFile(XFile file);

        boolean acceptDirectory(XFile file);

        boolean filterDirectory(XFile file);
    }

    public interface FileSearchCallback {
        @WorkerThread
        void onStart();

        @WorkerThread
        void onEachFile(XFile file);

        @WorkerThread
        void onFinish();
    }
}