package com.ydl.ydlnet.cache;

import android.os.Environment;
import android.os.StatFs;
import android.support.annotation.NonNull;
import com.ydl.ydlnet.cache.data.CacheResult;
import com.ydl.ydlnet.cache.diskconverter.IDiskConverter;
import com.ydl.ydlnet.cache.diskconverter.SerializableDiskConverter;
import com.ydl.ydlnet.cache.stategy.IFlowableStrategy;
import com.ydl.ydlnet.cache.stategy.IObservableStrategy;
import com.ydl.ydlnet.cache.utils.RxCacheLogUtils;
import io.reactivex.*;
import org.reactivestreams.Publisher;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.security.MessageDigest;


/**
 * RxJava remote data cache processing library, support Serializable, JSON
 * 作者: 赵成柱 on 2016/9/9 0012.
 */
public final class RxCache {


    private final CacheCore cacheCore;

    private static RxCache sDefaultRxCache;

    @NonNull
    public static RxCache getDefault() {
        if (sDefaultRxCache == null) {
            sDefaultRxCache = new Builder()
                    .appVersion(1)
                    .diskDir(Environment.getDownloadCacheDirectory())
                    .diskConverter(new SerializableDiskConverter())
                    .setDebug(true)
                    .build();

        }
        return sDefaultRxCache;
    }

    public static void initializeDefault(RxCache rxCache) {
        if (sDefaultRxCache == null) {
            RxCache.sDefaultRxCache = rxCache;
        } else {
            RxCacheLogUtils.log("You need to initialize it before using the default rxCache and only initialize it once");
        }
    }


    private RxCache(Builder builder) {
        LruMemoryCache memoryCache = null;
        if (builder.memoryMaxSize > 0) {
            memoryCache = new LruMemoryCache(builder.memoryMaxSize);
        }
        LruDiskCache lruDiskCache = null;
        if (builder.diskMaxSize > 0) {
            lruDiskCache = new LruDiskCache(builder.diskConverter, builder.diskDir, builder.appVersion, builder.diskMaxSize);
        }
        cacheCore = new CacheCore(memoryCache, lruDiskCache);

    }

    /**
     * notice: Deprecated! Use {@link #transformObservable(String, Type, IObservableStrategy)} ()}  replace.
     */
    @Deprecated
    public <T> ObservableTransformer<T, CacheResult<T>> transformer(String key, Type type, IObservableStrategy strategy) {
        return transformObservable(key, type, strategy);
    }

    public <T> ObservableTransformer<T, CacheResult<T>> transformObservable(final String key, final Type type, final IObservableStrategy strategy) {
        return new ObservableTransformer<T, CacheResult<T>>() {
            @Override
            public ObservableSource<CacheResult<T>> apply(Observable<T> tObservable) {
                return strategy.execute(RxCache.this, key, tObservable, type);
            }
        };
    }

    public <T> FlowableTransformer<T, CacheResult<T>> transformFlowable(final String key, final Type type, final IFlowableStrategy strategy) {
        return new FlowableTransformer<T, CacheResult<T>>() {
            @Override
            public Publisher<CacheResult<T>> apply(Flowable<T> flowable) {
                return strategy.flow(RxCache.this, key, flowable, type);
            }
        };
    }


    /**
     * 读取
     */
    public <T> Observable<CacheResult<T>> load(final String key, final Type type) {
        return Observable.create(new ObservableOnSubscribe<CacheResult<T>>() {
            @Override
            public void subscribe(ObservableEmitter<CacheResult<T>> observableEmitter) throws Exception {
                CacheResult<T> load = cacheCore.load(getMD5MessageDigest(key), type);
                if (!observableEmitter.isDisposed()) {
                    if (load != null) {
                        observableEmitter.onNext(load);
                        observableEmitter.onComplete();
                    } else {
                        observableEmitter.onError(new NullPointerException("Not find the key corresponding to the cache"));
                    }
                }
            }
        });
    }

    /**
     * 读取
     */
    public <T> Flowable<CacheResult<T>> load2Flowable(String key, Type type) {
        return load2Flowable(key, type, BackpressureStrategy.LATEST);
    }

    public <T> Flowable<CacheResult<T>> load2Flowable(final String key, final Type type, BackpressureStrategy backpressureStrategy) {
        return Flowable.create(new FlowableOnSubscribe<CacheResult<T>>() {
            @Override
            public void subscribe(FlowableEmitter<CacheResult<T>> flowableEmitter) throws Exception {
                CacheResult<T> load = cacheCore.load(getMD5MessageDigest(key), type);
                if (!flowableEmitter.isCancelled()) {
                    if (load != null) {
                        flowableEmitter.onNext(load);
                        flowableEmitter.onComplete();
                    } else {
                        flowableEmitter.onError(new NullPointerException("Not find the key corresponding to the cache"));
                    }
                }
            }
        }, backpressureStrategy);
    }


    public <T> Observable<Boolean> save(final String key, final T value) {
        return save(key, value, CacheTarget.MemoryAndDisk);
    }

    /**
     * 保存
     */
    public <T> Observable<Boolean> save(final String key, final T value, final CacheTarget target) {
        return Observable.create(new ObservableOnSubscribe<Boolean>() {
            @Override
            public void subscribe(ObservableEmitter<Boolean> observableEmitter) throws Exception {
                boolean save = cacheCore.save(getMD5MessageDigest(key), value, target);
                if (!observableEmitter.isDisposed()) {
                    observableEmitter.onNext(save);
                    observableEmitter.onComplete();
                }
            }
        });
    }

    /**
     * 保存
     */
    public <T> Flowable<Boolean> save2Flowable(final String key, final T value, final CacheTarget target) {
        return save2Flowable(key, value, target, BackpressureStrategy.LATEST);
    }

    /**
     * 保存
     */
    public <T> Flowable<Boolean> save2Flowable(final String key, final T value, final CacheTarget target, BackpressureStrategy strategy) {
        return Flowable.create(new FlowableOnSubscribe<Boolean>() {
            @Override
            public void subscribe(FlowableEmitter<Boolean> flowableEmitter) throws Exception {
                boolean save = cacheCore.save(getMD5MessageDigest(key), value, target);
                if (!flowableEmitter.isCancelled()) {
                    flowableEmitter.onNext(save);
                    flowableEmitter.onComplete();
                }
            }
        }, strategy);
    }

    /**
     * 是否包含
     *
     * @param key
     * @return
     */
    public boolean containsKey(final String key) {
        return cacheCore.containsKey(getMD5MessageDigest(key));
    }

    /**
     * 删除缓存
     */
    public boolean remove(final String key) {
        return cacheCore.remove(getMD5MessageDigest(key));
    }

    /**
     * 清空缓存
     */
    public Observable<Boolean> clear() {
        return Observable.create(new ObservableOnSubscribe<Boolean>() {
            @Override
            public void subscribe(ObservableEmitter<Boolean> observableEmitter) throws Exception {
                try {
                    cacheCore.clear();
                    if (!observableEmitter.isDisposed()) {
                        observableEmitter.onNext(true);
                        observableEmitter.onComplete();
                    }
                } catch (IOException e) {
                    RxCacheLogUtils.log(e);
                    if (!observableEmitter.isDisposed()) {
                        observableEmitter.onError(e);
                    }
                }
            }
        });
    }

    /**
     * 清空缓存
     */
    public void clear2() throws IOException {
        cacheCore.clear();
    }


    /**
     * 构造器
     */
    public static final class Builder {
        private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB
        private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
        private static final int DEFAULT_MEMORY_CACHE_SIZE = (int) (Runtime.getRuntime().maxMemory() / 8);//运行内存的8分之1
        private Integer memoryMaxSize;
        private Long diskMaxSize;
        private int appVersion;
        private File diskDir;
        private IDiskConverter diskConverter;

        public Builder() {
        }

        /**
         * 不设置,默认为运行内存的8分之1.设置0,或小于0，则不开启内存缓存;
         */
        public Builder memoryMax(int maxSize) {
            this.memoryMaxSize = maxSize;
            return this;
        }

        /**
         * 不设置，默认为1.需要注意的是,每当版本号改变,缓存路径下存储的所有数据都会被清除掉,所有的数据都应该从网上重新获取.
         */
        public Builder appVersion(int appVersion) {
            this.appVersion = appVersion;
            return this;
        }

        public Builder diskDir(File directory) {
            this.diskDir = directory;
            return this;
        }


        public Builder diskConverter(IDiskConverter converter) {
            this.diskConverter = converter;
            return this;
        }

        /**
         * 不设置， 默为认50MB.设置0,或小于0，则不开启硬盘缓存;
         */
        public Builder diskMax(long maxSize) {
            this.diskMaxSize = maxSize;
            return this;
        }

        public Builder setDebug(boolean debug) {
            RxCacheLogUtils.DEBUG = debug;
            return this;
        }

        public RxCache build() {
            if (this.diskDir == null) {
                throw new NullPointerException("DiskDir can not be null.");
            }
            if (!diskDir.exists() && !diskDir.mkdirs()) {
                throw new RuntimeException("can't make dirs in " + diskDir.getAbsolutePath());
            }
            if (this.diskConverter == null) {
                this.diskConverter = new SerializableDiskConverter();
            }
            if (memoryMaxSize == null) {
                memoryMaxSize = DEFAULT_MEMORY_CACHE_SIZE;
            }
            if (diskMaxSize == null) {
                diskMaxSize = calculateDiskCacheSize(diskDir);
            }
            appVersion = Math.max(1, this.appVersion);
            return new RxCache(this);
        }

        private static long calculateDiskCacheSize(File dir) {
            long size = 0;

            try {
                StatFs statFs = new StatFs(dir.getAbsolutePath());
                long available = ((long) statFs.getBlockCount()) * statFs.getBlockSize();
                // Target 2% of the total space.
                size = available / 50;
            } catch (IllegalArgumentException ignored) {
                RxCacheLogUtils.log(ignored);
            }
            // Bound inside min/max size for disk cache.
            return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE);
        }

    }

    static String getMD5MessageDigest(String buffer) {
        char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
        try {
            MessageDigest mdTemp = MessageDigest.getInstance("MD5");
            mdTemp.update(buffer.getBytes());
            byte[] md = mdTemp.digest();
            int j = md.length;
            char str[] = new char[j * 2];
            int k = 0;
            for (byte byte0 : md) {
                str[k++] = hexDigits[byte0 >>> 4 & 0xf];
                str[k++] = hexDigits[byte0 & 0xf];
            }
            return new String(str);
        } catch (Exception e) {
            RxCacheLogUtils.log(e);
            return buffer;
        }
    }

}
