package me.shouheng.vmlib.network.download;

import android.annotation.SuppressLint;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresPermission;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.TimeUnit;

import me.shouheng.vmlib.network.download.interceptor.ProgressInterceptor;
import me.shouheng.vmlib.network.download.interceptor.ProgressResponseCallback;
import me.shouheng.utils.device.NetworkUtils;
import me.shouheng.utils.stability.L;
import me.shouheng.utils.store.IOUtils;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

import static android.Manifest.permission.ACCESS_NETWORK_STATE;
import static android.Manifest.permission.ACCESS_WIFI_STATE;
import static android.Manifest.permission.INTERNET;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;

/**
 * The downloader used to download files from network. Sample
 *
 * <code>
 * Downloader.getInstance()
 *     .setOnlyWifi(true)
 *     .download(downloadUrl, PathUtils.getExternalStoragePath(), object : DownloadListener {
 *         override fun onError(errorCode: Int) {
 *             showShort("Download : error $errorCode")
 *         }
 *
 *         override fun onStart() {
 *             showShort("Download : start")
 *         }
 *
 *         override fun onProgress(readLength: Long, contentLength: Long) {
 *             L.d("Download : onProgress $readLength/$contentLength")
 *         }
 *
 *         override fun onComplete(file: File?) {
 *             showShort("Download : complete : ${file?.absoluteFile}")
 *         }
 *     })
 * </code>
 *
 * @author <a href="mailto:[email protected]">WngShhng</a>
 */
public final class Downloader {

    /**
     * Error code: the network is unavailable.
     */
    public static final int ERROR_CODE_NETWORK_UNAVAILABLE  = 100001;

    /**
     * Error code: only wifi and the wifi is unavailable.
     */
    public static final int ERROR_CODE_WIFI_UNAVAILABLE     = 100002;

    /**
     * Error code: no response body for request.
     */
    public static final int ERROR_CODE_NO_RESPONSE_BODY     = 100003;

    /**
     * Error code: failed to write file to file system.
     */
    public static final int ERROR_CODE_IO                   = 100004;

    /**
     * error code: other network error
     */
    public static final int ERROR_CODE_NETWORK              = 100005;

    /**
     * Seconds to timeout when download
     */
    private static final int TIME_OUT_SECONDS = 20;

    private DownloadListener downloadListener;

    private OkHttpClient okHttpClient;

    private Call requestCall;

    private boolean onlyWifi;

    private String url;

    private String filePath;

    private String fileName;

    private Handler mainThreadHandler;

    public static Downloader getInstance() {
        return new Downloader();
    }

    /**
     * Get file name from url, might be null if failed to parse url.
     *
     * @param imgUrl image url of string
     * @return       image url
     */
    @Nullable
    public static String getFileName(String imgUrl) {
        try {
            URL url = new URL(imgUrl);
            return new File(url.getFile()).getName();
        } catch (MalformedURLException ex) {
            L.e(ex);
            return null;
        }
    }

    private Downloader() {
        okHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new ProgressInterceptor(new ProgressResponseCallback() {
                    @Override
                    public void onProgressChanged(long contentLength, long readLength) {
                        if (downloadListener != null) {
                            downloadListener.onProgress(readLength, contentLength);
                        }
                    }
                }))
                .connectTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS)
                .build();
        mainThreadHandler = new Handler(Looper.getMainLooper());
    }

    /**
     * Set download only in wifi environment
     *
     * @param onlyWifi only wifi env
     * @return         the downloader
     */
    public Downloader setOnlyWifi(boolean onlyWifi) {
        this.onlyWifi = onlyWifi;
        return this;
    }

    /**
     * Download file of given url. The program will get file name from url.
     *
     * @param url              the remote url
     * @param filePath         the file path that the file was saved to
     * @param downloadListener the download progress callback
     */
    @RequiresPermission(allOf = {ACCESS_WIFI_STATE, INTERNET, ACCESS_NETWORK_STATE, WRITE_EXTERNAL_STORAGE})
    public void download(@NonNull String url, @NonNull String filePath, @Nullable DownloadListener downloadListener) {
        this.download(url, filePath, getFileName(url), downloadListener);
    }

    /**
     * Download file of given url.
     *
     * @param url              the url
     * @param filePath         the file path to save file
     * @param fileName         the file name of downloaded file
     * @param downloadListener the download state callback
     */
    @RequiresPermission(allOf = {ACCESS_WIFI_STATE, INTERNET, ACCESS_NETWORK_STATE, WRITE_EXTERNAL_STORAGE})
    public void download(@NonNull String url, @NonNull String filePath, @Nullable String fileName, @Nullable DownloadListener downloadListener) {
        this.downloadListener = downloadListener;
        this.url = url;
        this.filePath = filePath;
        if (fileName == null) L.w("The parameter 'fileName' was null, timestamp will be used.");
        this.fileName = fileName == null ? String.valueOf(System.currentTimeMillis()) : fileName;

        if (!NetworkUtils.isConnected()) {
            notifyDownloadError(ERROR_CODE_NETWORK_UNAVAILABLE);
        } else {
            if (onlyWifi) {
                checkWifiAvailable();
            } else {
                doDownload();
            }
        }
    }

    /**
     * Cancel the request
     */
    public void cancel() {
        if (requestCall != null && !requestCall.isCanceled()) {
            requestCall.cancel();
        }
    }

    /*--------------------------------------------inner methods-------------------------------------------*/

    /**
     * Check wifi availability in background thread,
     * since it might block the ui thread.
     */
    private void checkWifiAvailable() {
        new WifiChecker(new WifiChecker.WifiStateListener() {
            @Override
            public void onGetWifiState(boolean available) {
                if (available) {
                    doDownload();
                } else {
                    notifyDownloadError(ERROR_CODE_WIFI_UNAVAILABLE);
                }
            }
        }).execute();
    }

    private void doDownload() {
        notifyDownloadStart();
        requestCall = okHttpClient.newCall(
                new Request.Builder()
                        .url(url)
                        .build()
        );
        requestCall.enqueue(new Callback() {
            @Override
            public void onFailure(@NonNull Call call, @NonNull IOException e) {
                notifyDownloadError(ERROR_CODE_NETWORK);
            }

            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) {
                ResponseBody body = response.body();
                try {
                    InputStream is;
                    if (body != null) {
                        is = body.byteStream();
                    } else {
                        notifyDownloadError(ERROR_CODE_NO_RESPONSE_BODY);
                        return;
                    }
                    File fileSaveTo = new File(filePath, fileName);
                    boolean succeed = IOUtils.writeFileFromIS(fileSaveTo, is);
                    if (!succeed) {
                        notifyDownloadError(ERROR_CODE_IO);
                    } else {
                        notifyDownloadComplete(fileSaveTo);
                    }
                } catch (Exception ex) {
                    notifyDownloadError(ERROR_CODE_IO);
                }
            }
        });
    }

    private void notifyDownloadStart() {
        mainThreadHandler.post(new Runnable() {
            @Override
            public void run() {
                if (downloadListener != null) {
                    downloadListener.onStart();
                }
            }
        });
    }

    private void notifyDownloadComplete(final File fileSaveTo) {
        mainThreadHandler.post(new Runnable() {
            @Override
            public void run() {
                if (downloadListener != null) {
                    downloadListener.onComplete(fileSaveTo);
                }
            }
        });
    }

    private void notifyDownloadError(final int errorCode) {
        mainThreadHandler.post(new Runnable() {
            @Override
            public void run() {
                if (downloadListener != null) {
                    downloadListener.onError(errorCode);
                }
            }
        });
    }

    private static class WifiChecker extends AsyncTask<Void, Void, Boolean> {

        private WifiStateListener wifiStateListener;

        WifiChecker(WifiStateListener wifiStateListener) {
            this.wifiStateListener = wifiStateListener;
        }

        @SuppressLint("MissingPermission")
        @Override
        protected Boolean doInBackground(Void... voids) {
            return NetworkUtils.isWifiAvailable();
        }

        @Override
        protected void onPostExecute(Boolean aBoolean) {
            if (wifiStateListener != null) {
                wifiStateListener.onGetWifiState(aBoolean);
            }
        }

        public interface WifiStateListener {
            void onGetWifiState(boolean available);
        }
    }
}