package com.datarecovery.master.utils.filedetect;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Environment;
import android.text.TextUtils;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;

import com.atmob.common.crypto.CryptoUtils;
import com.datarecovery.master.utils.ImageDeepDetector;
import com.datarecovery.master.utils.xfile.XFile;
import com.datarecovery.master.utils.xfile.XPathFile;

import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;

import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.zip.Adler32;

import atmob.reactivex.rxjava3.core.Flowable;
import atmob.reactivex.rxjava3.schedulers.Schedulers;

public class ImageCacheUtil {

    public static boolean isImageSuffix(String name) {
        if (TextUtils.isEmpty(name)) {
            return false;
        }
        return name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".png")
                || name.endsWith(".gif") || name.endsWith(".bmp") || name.endsWith(".webp")
                || name.endsWith(".tiff") || name.endsWith(".psd") || name.endsWith(".svg")
                || name.endsWith(".raw") || name.endsWith(".heif") || name.endsWith(".indd");
    }

    public static boolean isHuaweiGalleryCacheFile(String path) {
        if (TextUtils.isEmpty(path)) {
            return false;
        }
        if (!path.contains("com.huawei.photos%2Ffiles%2Fthumbdb") &&
                !path.contains("com.huawei.photos/files/thumbdb")) {
            return false;
        }
        return path.endsWith("photoshare.db");
    }

    public static boolean isMeizuGalleryCacheFile(String path) {
        if (TextUtils.isEmpty(path)) {
            return false;
        }
        if (!path.contains("com.meizu.media.gallery%2Fcache") &&
                !path.contains("com.meizu.media.gallery/cache")) {
            return false;
        }
        return path.contains("bestPhotoCache") || path.contains("face_thumbnails")
                || path.contains("uri_thumbnail_cache");
    }

    public static boolean isXiaomiGalleryCacheFile(String path) {
        if (TextUtils.isEmpty(path)) {
            return false;
        }
        if (!path.contains("com.miui.gallery%2Ffiles%2Fgallery_disk_cache") &&
                !path.contains("com.miui.gallery/files/gallery_disk_cache")) {
            return false;
        }
        return path.contains("full_size") || path.contains("small_size");
    }

    public static boolean isVivoGalleryCacheFile(String path) {
        if (TextUtils.isEmpty(path)) {
            return false;
        }
        if (!path.contains("com.vivo.gallery%2Fcache") &&
                !path.contains("com.vivo.gallery/cache")) {
            return false;
        }
        return path.contains("imgcache") || path.contains("trackthumbnail_cache");
    }

    public static boolean isOppoGalleryCacheFile(String path) {
        if (TextUtils.isEmpty(path)) {
            return false;
        }
        if (!path.contains("com.coloros.gallery3d%2Fcache") &&
                !path.contains("com.coloros.gallery3d/cache")) {
            return false;
        }
        return path.contains("imgcache") || path.contains("screennailcache")
                || path.contains("tilecache");
    }

    public static boolean hasImgMagic(XFile file) {
        try {
            if (file.getMineType() != null && !file.getMineType().equals("application/octet-stream")) {
                return false;
            }
        } catch (Exception ignore) {
        }
        try (InputStream inputStream = file.newInputStream()) {
            byte[] bytes = new byte[8];
            if (inputStream.read(bytes) != 8) {
                return false;
            }

            if (bytes[0] == (byte) 0x89 && bytes[1] == (byte) 0x50 && bytes[2] == (byte) 0x4E && bytes[3] == (byte) 0x47
                    && bytes[4] == (byte) 0x0D && bytes[5] == (byte) 0x0A && bytes[6] == (byte) 0x1A && bytes[7] == (byte) 0x0A) {
                // png
                return true;
            }

            boolean hasHeaderMagic = bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xD8 && bytes[2] == (byte) 0xFF; // jpg header
            if (!hasHeaderMagic) {
                return false;
            }

            long skip = inputStream.available() - 2;
            if (inputStream.skip(skip) != skip) {
                return false;
            }
            if (inputStream.read(bytes) != 2) {
                return false;
            }
            return bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xD9;
        } catch (Exception ignore) {
        }
        return false;
    }

    public static Flowable<DetectFile> detectOppoGalleryCache(Context context, XFile xFile) {
        return new GenericImgCollectionDetector(context, xFile, FileType.IMAGE_GALLERY)
                .subscribeOn(Schedulers.io())
                .onErrorComplete();
    }

    public static Flowable<DetectFile> detectGalleryCache(@NonNull Context context, @NonNull XFile xFile) {
        return new GalleryCacheDetector(context, xFile)
                .subscribeOn(Schedulers.io())
                .onErrorComplete();
    }

    public static Publisher<DetectFile> detectWechatCache(@NonNull Context context, @NonNull XFile xFile) {
        return new WechatCacheDetector(context, xFile)
                .subscribeOn(Schedulers.io())
                .onErrorComplete();
    }

    public static Publisher<DetectFile> detectVivoGalleryCache(@NonNull Context context, @NonNull XFile xFile) {
        return new GenericImgCollectionDetector(context, xFile, FileType.IMAGE_GALLERY)
                .subscribeOn(Schedulers.io())
                .onErrorComplete();
    }

    public static Publisher<DetectFile> detectMeizuGalleryCache(@NonNull Context context, @NonNull XFile xFile) {
        return new GenericImgCollectionDetector(context, xFile, FileType.IMAGE_GALLERY)
                .subscribeOn(Schedulers.io())
                .onErrorComplete();
    }

    public static Publisher<DetectFile> detectHuaweiGalleryCache(@NonNull Context context, @NonNull XFile xFile) {
        return new HuaweiGalleryCacheDetector(context, xFile)
                .subscribeOn(Schedulers.io())
                .onErrorComplete();
    }

    private static class HuaweiGalleryCacheDetector extends Flowable<DetectFile> {

        private static final String CACHE_DOMAIN = "huawei_gallery_cache_detector";
        private final Context context;
        private final XFile dbFile;

        public HuaweiGalleryCacheDetector(Context context, XFile dbFile) {
            this.context = context;
            this.dbFile = dbFile;
        }

        @Override
        protected void subscribeActual(@NonNull Subscriber<? super DetectFile> subscriber) {
            long lastModified;
            try {
                lastModified = dbFile.lastModified();
            } catch (Exception e) {
                subscriber.onError(e);
                return;
            }

            if (checkDetectedCache(context, lastModified, subscriber)) {
                subscriber.onComplete();
                return;
            } else {
                clearDetectedCache(context, CACHE_DOMAIN);
            }

            File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
            detectedCacheDir = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
            if (!detectedCacheDir.exists()) {
                detectedCacheDir.mkdirs();
            }

            File dbTempFile;
            try {
                dbTempFile = createDbTempFile(detectedCacheDir);
            } catch (Exception e) {
                subscriber.onError(e);
                return;
            }

            try (SQLiteDatabase sqLiteDatabase = SQLiteDatabase.openDatabase(dbTempFile.getPath(), null, SQLiteDatabase.OPEN_READONLY);
                 Cursor cursor = sqLiteDatabase.rawQuery("select * from general_kv", null)
            ) {
                int vIndex = cursor.getColumnIndex("v");
                if (vIndex == -1) {
                    subscriber.onComplete();
                    return;
                }
                while (cursor.moveToNext()) {
                    byte[] data = cursor.getBlob(vIndex);
                    if (data == null || data.length == 0) {
                        continue;
                    }
                    File cache = new File(detectedCacheDir, UUID.randomUUID().toString());
                    if (cache.createNewFile() && bytes2File(data, cache)) {
                        subscriber.onNext(new DetectFile(new XPathFile(context, cache), FileType.IMAGE_GALLERY));
                    }
                }
                subscriber.onComplete();
            } catch (Exception e) {
                subscriber.onError(e);
            } finally {
                if (dbTempFile != null && dbTempFile.exists()) {
                    dbTempFile.delete();
                }
            }
        }

        private File createDbTempFile(File detectedCacheDir) throws Exception {
            File dbTempFile = new File(detectedCacheDir, "huawei_gallery_cache_detector.db");
            if (dbTempFile.exists()) {
                dbTempFile.delete();
            }
            try (InputStream inputStream = dbFile.newInputStream();
                 OutputStream outputStream = new FileOutputStream(dbTempFile)
            ) {
                byte[] buffer = new byte[2048];
                int read;
                while ((read = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, read);
                }
                outputStream.flush();
                return dbTempFile;
            }
        }

        private boolean checkDetectedCache(Context context, long lastModified, Subscriber<? super DetectFile> subscriber) {
            File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
            File targetCaches = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
            File[] files = targetCaches.exists() && targetCaches.isDirectory() ? targetCaches.listFiles() : null;
            if (files != null && files.length > 0) {
                for (File file : files) {
                    if (file.getName().endsWith(".db")) {
                        continue;
                    }
                    subscriber.onNext(new DetectFile(new XPathFile(this.context, file), FileType.IMAGE_GALLERY));
                }
                return true;
            }
            return false;
        }
    }

    private static class GenericImgCollectionDetector extends Flowable<DetectFile> {

        private final int category;
        private String CACHE_DOMAIN = "generic_img_collection_detector";
        private final Context context;
        private final XFile xFile;

        public GenericImgCollectionDetector(Context context, XFile xFile, @FileType int category) {
            this.context = context;
            this.xFile = xFile;
            this.category = category;
        }

        @Override
        protected void subscribeActual(@NonNull Subscriber<? super DetectFile> subscriber) {
            long lastModified;
            try {
                lastModified = xFile.lastModified();
                CACHE_DOMAIN += xFile.getName();
            } catch (Exception e) {
                subscriber.onError(e);
                return;
            }

            if (checkDetectedCache(context, lastModified, subscriber)) {
                subscriber.onComplete();
                return;
            } else {
                clearDetectedCache(context, CACHE_DOMAIN);
            }

            File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
            detectedCacheDir = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
            if (!detectedCacheDir.exists()) {
                detectedCacheDir.mkdirs();
            }

            try (InputStream inputStream = xFile.newInputStream()) {
                ArrayList<Byte> imageBytes = new ArrayList<>();
                byte[] buffer = new byte[2048];
                int read;
                while ((read = inputStream.read(buffer)) != -1) {
                    for (int i = 0; i < read; i++) {
                        byte b = buffer[i];
                        imageBytes.add(b);
                        if (imageBytes.size() < 3) {
                            continue;
                        }
                        if (imageBytes.size() == 3) {
                            if (imageBytes.get(0) != (byte) 0xFF || imageBytes.get(1) != (byte) 0xD8 || imageBytes.get(2) != (byte) 0xFF) {
                                imageBytes.remove(0);
                            }
                            continue;
                        }
                        if (i == read - 1 && inputStream.available() == 0) {
                            if (imageBytes.get(imageBytes.size() - 2) == (byte) 0xFF && imageBytes.get(imageBytes.size() - 1) == (byte) 0xD9) {
                                File cache = new File(detectedCacheDir, UUID.randomUUID().toString());
                                if (cache.createNewFile() && bytes2File(imageBytes, cache)) {
                                    subscriber.onNext(new DetectFile(new XPathFile(context, cache), category));
                                }
                            }
                            imageBytes.clear();
                        } else if (imageBytes.size() >= 5) {
                            if (imageBytes.get(imageBytes.size() - 1) == (byte) 0xD9
                                    && imageBytes.get(imageBytes.size() - 2) == (byte) 0xFF
                            ) {
                                File cache = new File(detectedCacheDir, UUID.randomUUID().toString());
                                if (cache.createNewFile() && bytes2File(imageBytes, cache)) {
                                    subscriber.onNext(new DetectFile(new XPathFile(context, cache), category));
                                }
                                imageBytes.clear();
                            }
                        }
                    }
                }
                subscriber.onComplete();
            } catch (Exception e) {
                subscriber.onError(e);
            }
        }

        private boolean checkDetectedCache(Context context, long lastModified, Subscriber<? super DetectFile> subscriber) {
            File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
            File targetCaches = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
            File[] files = targetCaches.exists() && targetCaches.isDirectory() ? targetCaches.listFiles() : null;
            if (files != null && files.length > 0) {
                for (File file : files) {
                    subscriber.onNext(new DetectFile(new XPathFile(this.context, file), category));
                }
                return true;
            }
            return false;
        }
    }

    private static class GalleryCacheDetector extends Flowable<DetectFile> {

        private static final String CACHE_DOMAIN = "gallery_cache_detector";
        private static final int MAGIC_INDEX_FILE = 0xB3273030;
        private static final int MAGIC_DATA_FILE = 0xBD248510;

        // index header offset
        private static final int IH_MAGIC = 0;
        private static final int IH_MAX_ENTRIES = 4;
        private static final int IH_MAX_BYTES = 8;
        private static final int IH_ACTIVE_REGION = 12;
        private static final int IH_ACTIVE_ENTRIES = 16;
        private static final int IH_ACTIVE_BYTES = 20;
        private static final int IH_CHECKSUM = 28;
        private static final int INDEX_HEADER_SIZE = 32;

        private static final int DATA_HEADER_SIZE = 4;

        // blob header offset
        private static final int BH_KEY = 0;
        private static final int BH_CHECKSUM = 8;
        private static final int BH_OFFSET = 12;
        private static final int BH_LENGTH = 16;
        private static final int BLOB_HEADER_SIZE = 20;
        private final XFile galleryCacheDir;
        private final byte[] indexHeader;
        private final byte[] blobHeader;
        private final Adler32 mAdler32 = new Adler32();
        private final Context context;

        private int mMaxEntries;
        private int mMaxBytes;
        private int mActiveRegion;
        private int mActiveBytes;

        private FileChannel mIndexChannel;
        private MappedByteBuffer mIndexBuffer;
        private RandomAccessFile mIndexFile;
        private RandomAccessFile mDataFile0;
        private RandomAccessFile mDataFile1;

        private RandomAccessFile mActiveDataFile;
        private int mActiveHashStart;
        private File indexTemp;
        private File data0Temp;
        private File data1Temp;

        public GalleryCacheDetector(Context context, XFile galleryCacheDir) {
            this.context = context;
            this.galleryCacheDir = galleryCacheDir;
            this.indexHeader = new byte[INDEX_HEADER_SIZE];
            this.blobHeader = new byte[BLOB_HEADER_SIZE];
        }

        @Override
        protected void subscribeActual(@NonNull Subscriber<? super DetectFile> subscriber) {
            XFile[] xFiles;
            try {
                xFiles = galleryCacheDir.listFiles();
            } catch (Exception e) {
                subscriber.onError(e);
                return;
            }
            if (xFiles == null || xFiles.length == 0) {
                subscriber.onComplete();
                return;
            }
            XFile indexFile = null;
            XFile dataFile0 = null;
            XFile dataFile1 = null;
            for (XFile xFile : xFiles) {
                try {
                    String name = xFile.getName();
                    if (name.endsWith(".idx")) {
                        indexFile = xFile;
                    } else if (name.endsWith(".0")) {
                        dataFile0 = xFile;
                    } else if (name.endsWith(".1")) {
                        dataFile1 = xFile;
                    }
                } catch (Exception e) {
                    subscriber.onError(e);
                }
            }
            if (indexFile == null || dataFile0 == null || dataFile1 == null) {
                subscriber.onComplete();
                return;
            }
            doDetect(indexFile, dataFile0, dataFile1, subscriber);
        }

        private void doDetect(XFile indexFile, XFile dataFile0, XFile dataFile1, Subscriber<? super DetectFile> subscriber) {
            try {
                long lastModified;
                try {
                    lastModified = indexFile.lastModified();
                } catch (Exception e) {
                    subscriber.onError(e);
                    return;
                }

                if (checkDetectedCache(context, lastModified, subscriber)) {
                    subscriber.onComplete();
                    return;
                } else {
                    clearDetectedCache(context, CACHE_DOMAIN);
                }

                File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
                detectedCacheDir = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
                if (!detectedCacheDir.exists()) {
                    detectedCacheDir.mkdirs();
                }

                loadIndex(indexFile, dataFile0, dataFile1);
                for (int i = 0; i < mMaxEntries; i++) {
                    int offset = mActiveHashStart + i * 12;
                    long candidateKey = mIndexBuffer.getLong(offset);
                    try {
                        GalleryCacheDetector.LookupRequest lookupRequest = new GalleryCacheDetector.LookupRequest(candidateKey);
                        if (!lookup(lookupRequest)) {
                            continue;
                        }
                        byte[] lookup = lookupRequest.buffer;
                        if (lookup == null) {
                            continue;
                        }
                        byte[] cropData = cropLookup(lookup);
                        if (cropData == null) {
                            continue;
                        }
                        File cache = new File(detectedCacheDir, UUID.randomUUID().toString());
                        if (cache.createNewFile() && bytes2File(cropData, cache)) {
                            subscriber.onNext(new DetectFile(new XPathFile(context, cache), FileType.IMAGE_GALLERY));
                        }
                    } catch (Exception ignore) {
                    }
                }
                subscriber.onComplete();
            } catch (Exception e) {
                subscriber.onError(e);
            } finally {
                closeAll();
                deleteTempFiles();
            }
        }

        private boolean checkDetectedCache(Context context, long lastModified, Subscriber<? super DetectFile> subscriber) {
            File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
            File targetCaches = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
            File[] files = targetCaches.exists() && targetCaches.isDirectory() ? targetCaches.listFiles() : null;
            if (files != null && files.length > 0) {
                for (File file : files) {
                    subscriber.onNext(new DetectFile(new XPathFile(this.context, file), FileType.IMAGE_GALLERY));
                }
                return true;
            }
            return false;
        }

        private byte[] cropLookup(byte[] lookup) {
            for (int i = 0; i < lookup.length; i++) {
                if (lookup[i] == (byte) 0xFF && i + 1 < lookup.length && lookup[i + 1] == (byte) 0xD8) {
                    return crop(lookup, i);
                }
            }
            return null;
        }

        private byte[] crop(byte[] lookup, int i) {
            byte[] crop = new byte[lookup.length - i];
            System.arraycopy(lookup, i, crop, 0, crop.length);
            return crop;
        }

        private void loadIndex(XFile indexFile, XFile dataFile0, XFile dataFile1) throws Exception {
            checkFileValid(indexFile, dataFile0, dataFile1);

            try (InputStream idxIs = indexFile.newInputStream();
                 InputStream data0Is = dataFile0.newInputStream();
                 InputStream data1Is = dataFile1.newInputStream()
            ) {
                indexTemp = createTempFile("index.temp", idxIs);
                mIndexFile = new RandomAccessFile(indexTemp, "rw");

                data0Temp = createTempFile("data0.temp", data0Is);
                mDataFile0 = new RandomAccessFile(data0Temp, "rw");

                data1Temp = createTempFile("data1.temp", data1Is);
                mDataFile1 = new RandomAccessFile(data1Temp, "rw");

                // Map index file to memory
                mIndexChannel = mIndexFile.getChannel();
                mIndexBuffer = mIndexChannel.map(FileChannel.MapMode.READ_WRITE,
                        0, mIndexFile.length());
                mIndexBuffer.order(ByteOrder.LITTLE_ENDIAN);

                setActiveVariables();
            }
        }

        private void setActiveVariables() throws Exception {
            mActiveDataFile = (mActiveRegion == 0) ? mDataFile0 : mDataFile1;
            mActiveDataFile.setLength(mActiveBytes);
            mActiveDataFile.seek(mActiveBytes);

            mActiveHashStart = INDEX_HEADER_SIZE;

            if (mActiveRegion != 0) {
                mActiveHashStart += mMaxEntries * 12;
            }
        }

        private void checkFileValid(XFile indexFile, XFile dataFile0, XFile dataFile1) throws Exception {
            byte[] buf = indexHeader;
            try (InputStream inputStream = indexFile.newInputStream()) {
                if (inputStream.read(buf) != INDEX_HEADER_SIZE) {
                    throw new Exception("cannot read header");
                }
                if (readInt(buf, IH_MAGIC) != MAGIC_INDEX_FILE) {
                    throw new Exception("cannot read header magic");
                }
            }

            mMaxEntries = readInt(buf, IH_MAX_ENTRIES);
            mMaxBytes = readInt(buf, IH_MAX_BYTES);
            mActiveRegion = readInt(buf, IH_ACTIVE_REGION);
            int mActiveEntries = readInt(buf, IH_ACTIVE_ENTRIES);
            mActiveBytes = readInt(buf, IH_ACTIVE_BYTES);

            int sum = readInt(buf, IH_CHECKSUM);
            if (checkSum(buf, 0, IH_CHECKSUM) != sum) {
                throw new Exception("header checksum does not match");
            }

            // Sanity check
            if (mMaxEntries <= 0) {
                throw new Exception("invalid max entries");
            }
            if (mMaxBytes <= 0) {
                throw new Exception("invalid max bytes");
            }
            if (mActiveRegion != 0 && mActiveRegion != 1) {
                throw new Exception("invalid active region");
            }
            if (mActiveEntries < 0 || mActiveEntries > mMaxEntries) {
                throw new Exception("invalid active entries");
            }
            if (mActiveBytes < DATA_HEADER_SIZE || mActiveBytes > mMaxBytes) {
                throw new Exception("invalid active bytes");
            }
            if (indexFile.length() != INDEX_HEADER_SIZE + mMaxEntries * 12 * 2L) {
                throw new Exception("invalid index file length");
            }

            // Make sure data file has magic
            byte[] magic = new byte[4];
            try (InputStream data0Is = dataFile0.newInputStream()) {
                if (data0Is.read(magic) != 4) {
                    throw new Exception("cannot read data file magic");
                }
            }
            if (readInt(magic, 0) != MAGIC_DATA_FILE) {
                throw new Exception("invalid data file magic");
            }
            try (InputStream data1Is = dataFile1.newInputStream()) {
                if (data1Is.read(magic) != 4) {
                    throw new Exception("cannot read data file magic");
                }
            }
            if (readInt(magic, 0) != MAGIC_DATA_FILE) {
                throw new Exception("invalid data file magic");
            }
        }

        private File createTempFile(String fileName, InputStream inputStream) throws Exception {
            File tempFile = new File(context.getCacheDir(), fileName);
            if (tempFile.exists()) {
                tempFile.delete();
            }
            if (!tempFile.createNewFile()) {
                throw new Exception("cannot create temp file");
            }
            try (OutputStream outputStream = new FileOutputStream(tempFile)) {
                copyStream(inputStream, outputStream);
            }
            return tempFile;
        }

        public boolean lookup(GalleryCacheDetector.LookupRequest req) throws IOException {
            if (lookupInternal(req.key, mActiveHashStart)) {
                return getBlob(mActiveDataFile, mFileOffset, req);
            }
            return false;
        }

        private int mFileOffset;

        private boolean lookupInternal(long key, int hashStart) {
            int slot = (int) (key % mMaxEntries);
            if (slot < 0) slot += mMaxEntries;
            int slotBegin = slot;
            while (true) {
                int offset = hashStart + slot * 12;
                long candidateKey = mIndexBuffer.getLong(offset);
                int candidateOffset = mIndexBuffer.getInt(offset + 8);
                if (candidateOffset == 0) {
                    return false;
                } else if (candidateKey == key) {
                    mFileOffset = candidateOffset;
                    return true;
                } else {
                    if (++slot >= mMaxEntries) {
                        slot = 0;
                    }
                    if (slot == slotBegin) {
                        mIndexBuffer.putInt(hashStart + slot * 12 + 8, 0);
                    }
                }
            }
        }

        private boolean getBlob(RandomAccessFile file, int offset,
                                GalleryCacheDetector.LookupRequest req) throws IOException {
            byte[] header = blobHeader;
            long oldPosition = file.getFilePointer();
            try {
                file.seek(offset);
                if (file.read(header) != BLOB_HEADER_SIZE) {
                    return false;
                }
                long blobKey = readLong(header, BH_KEY);
                if (blobKey == 0) {
                    return false; // This entry has been cleared.
                }
                if (blobKey != req.key) {
                    return false;
                }
                int sum = readInt(header, BH_CHECKSUM);
                int blobOffset = readInt(header, BH_OFFSET);
                if (blobOffset != offset) {
                    return false;
                }
                int length = readInt(header, BH_LENGTH);
                if (length < 0 || length > mMaxBytes - offset - BLOB_HEADER_SIZE) {
                    return false;
                }
                if (req.buffer == null || req.buffer.length < length) {
                    req.buffer = new byte[length];
                }

                byte[] blob = req.buffer;
                req.length = length;

                if (file.read(blob, 0, length) != length) {
                    return false;
                }
                return checkSum(blob, 0, length) == sum;
            } catch (Throwable t) {
                return false;
            } finally {
                file.seek(oldPosition);
            }
        }

        static int readInt(byte[] buf, int offset) {
            return (buf[offset] & 0xff)
                    | ((buf[offset + 1] & 0xff) << 8)
                    | ((buf[offset + 2] & 0xff) << 16)
                    | ((buf[offset + 3] & 0xff) << 24);
        }

        static long readLong(byte[] buf, int offset) {
            long result = buf[offset + 7] & 0xff;
            for (int i = 6; i >= 0; i--) {
                result = (result << 8) | (buf[offset + i] & 0xff);
            }
            return result;
        }

        int checkSum(byte[] data, int offset, int nbytes) {
            mAdler32.reset();
            mAdler32.update(data, offset, nbytes);
            return (int) mAdler32.getValue();
        }

        private void copyStream(InputStream is, OutputStream os) throws Exception {
            byte[] buf = new byte[2048];
            int n;
            while ((n = is.read(buf)) > 0) {
                os.write(buf, 0, n);
            }
            os.flush();
        }

        private void closeAll() {
            closeSilently(mIndexChannel);
            closeSilently(mIndexFile);
            closeSilently(mDataFile0);
            closeSilently(mDataFile1);
        }

        private void closeSilently(Closeable c) {
            if (c == null) return;
            try {
                c.close();
            } catch (Throwable t) {
                // do nothing
            }
        }

        private void deleteTempFiles() {
            deleteSilently(indexTemp);
            deleteSilently(data0Temp);
            deleteSilently(data1Temp);
        }

        private void deleteSilently(File tempFile) {
            if (tempFile == null) {
                return;
            }
            try {
                if (tempFile.exists()) {
                    tempFile.delete();
                }
            } catch (Throwable t) {
                // do nothing
            }
        }

        public static class LookupRequest {
            public long key;        // input: the key to find
            public byte[] buffer;   // input/output: the buffer to store the blob
            public int length;      // output: the length of the blob

            public LookupRequest(long key) {
                this.key = key;
            }
        }
    }

    private static class WechatCacheDetector extends Flowable<DetectFile> {
        private static final String CACHE_DOMAIN = "wechat_cache_detector";
        private final XFile xFile;
        private final Context context;

        public WechatCacheDetector(Context context, XFile xFile) {
            this.context = context;
            this.xFile = xFile;
        }

        @Override
        protected void subscribeActual(@NonNull Subscriber<? super DetectFile> subscriber) {
            long lastModified;
            try {
                lastModified = xFile.lastModified();
            } catch (Exception e) {
                subscriber.onError(e);
                return;
            }

            if (checkDetectedCache(context, lastModified, subscriber)) {
                subscriber.onComplete();
                return;
            } else {
                clearDetectedCache(context, CACHE_DOMAIN);
            }

            File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
            detectedCacheDir = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
            if (!detectedCacheDir.exists()) {
                detectedCacheDir.mkdirs();
            }

            try (InputStream inputStream = xFile.newInputStream()) {
                ArrayList<Byte> imageBytes = new ArrayList<>();
                byte[] buffer = new byte[2048];
                int read;
                while ((read = inputStream.read(buffer)) != -1) {
                    for (int i = 0; i < read; i++) {
                        byte b = buffer[i];
                        imageBytes.add(b);
                        if (imageBytes.size() < 2) {
                            continue;
                        }
                        if (imageBytes.size() == 2) {
                            if (imageBytes.get(0) != (byte) 0xFF || imageBytes.get(1) != (byte) 0xD8) {
                                imageBytes.remove(0);
                            }
                            continue;
                        }
                        if (i == read - 1 && inputStream.available() == 0) {
                            if (imageBytes.get(imageBytes.size() - 2) == (byte) 0xFF && imageBytes.get(imageBytes.size() - 1) == (byte) 0xD9) {
                                File cache = new File(detectedCacheDir, UUID.randomUUID().toString());
                                if (cache.createNewFile() && bytes2File(imageBytes, cache)) {
                                    subscriber.onNext(new DetectFile(new XPathFile(context, cache), FileType.IMAGE_WEIXIN));
                                }
                            }
                            imageBytes.clear();
                        } else if (imageBytes.size() >= 6) {
                            if (imageBytes.get(imageBytes.size() - 1) == (byte) 0xD8
                                    && imageBytes.get(imageBytes.size() - 2) == (byte) 0xFF
                                    && imageBytes.get(imageBytes.size() - 3) == (byte) 0xD9
                                    && imageBytes.get(imageBytes.size() - 4) == (byte) 0xFF
                            ) {
                                imageBytes.remove(imageBytes.size() - 1);
                                imageBytes.remove(imageBytes.size() - 1);
                                File cache = new File(detectedCacheDir, UUID.randomUUID().toString());
                                if (cache.createNewFile() && bytes2File(imageBytes, cache)) {
                                    subscriber.onNext(new DetectFile(new XPathFile(context, cache), FileType.IMAGE_WEIXIN));
                                }
                                imageBytes.clear();
                                imageBytes.add((byte) 0xFF);
                                imageBytes.add((byte) 0xD8);
                            }
                        }
                    }
                }
                subscriber.onComplete();
            } catch (Exception e) {
                subscriber.onError(e);
            }
        }

        private boolean checkDetectedCache(Context context, long lastModified, Subscriber<? super DetectFile> subscriber) {
            File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
            File targetCaches = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
            File[] files = targetCaches.exists() && targetCaches.isDirectory() ? targetCaches.listFiles() : null;
            if (files != null && files.length > 0) {
                for (File file : files) {
                    subscriber.onNext(new DetectFile(new XPathFile(this.context, file), FileType.IMAGE_WEIXIN));
                }
                return true;
            }
            return false;
        }
    }

    private static void clearDetectedCache(Context context, String domain) {
        File detectedCacheDir = getDetectedCacheDir(context, domain);
        try {
            clearDir(detectedCacheDir);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static boolean bytes2File(byte[] bytes, File file) {
        try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
            fileOutputStream.write(bytes);
            fileOutputStream.flush();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    private static boolean bytes2File(List<Byte> bytes, File file) {
        try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
            for (Byte aByte : bytes) {
                fileOutputStream.write(aByte);
            }
            fileOutputStream.flush();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    private static void clearDir(File dir) {
        if (dir == null || !dir.exists()) {
            return;
        }
        File[] files = dir.listFiles();
        if (files == null || files.length == 0) {
            dir.delete();
            return;
        }
        for (File file : files) {
            if (file.isDirectory()) {
                clearDir(file);
            } else {
                file.delete();
            }
        }
        dir.delete();
    }

    private static File getDetectedCacheDir(Context context, String domain) {
        File cacheDir;
        if (Objects.equals(Environment.getExternalStorageState(), Environment.MEDIA_MOUNTED)
                && Environment.getExternalStorageDirectory().canWrite()) {
            cacheDir = context.getExternalCacheDir();
        } else {
            cacheDir = context.getCacheDir();
        }
        File detectedCacheDir = new File(cacheDir, CryptoUtils.HASH.md5(domain));
        if (!detectedCacheDir.exists()) {
            detectedCacheDir.mkdirs();
        }
        return detectedCacheDir;
    }
}
