package us.shandian.giga.get;

import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import com.google.gson.Gson;

import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import us.shandian.giga.util.Utility;

import static org.schabi.newpipe.BuildConfig.DEBUG;

public class DownloadMission {
    private static final String TAG = DownloadMission.class.getSimpleName();

    public interface MissionListener {
        HashMap<MissionListener, Handler> handlerStore = new HashMap<>();

        void onProgressUpdate(DownloadMission downloadMission, long done, long total);

        void onFinish(DownloadMission downloadMission);

        void onError(DownloadMission downloadMission, int errCode);
    }

    public static final int ERROR_SERVER_UNSUPPORTED = 206;
    public static final int ERROR_UNKNOWN = 233;

    /**
     * The filename
     */
    public String name;

    /**
     * The url of the file to download
     */
    public String url;

    /**
     * The directory to store the download
     */
    public String location;

    /**
     * Number of blocks the size of {@link DownloadManager#BLOCK_SIZE}
     */
    public long blocks;

    /**
     * Number of bytes
     */
    public long length;

    /**
     * Number of bytes downloaded
     */
    public long done;
    public int threadCount = 3;
    public int finishCount;
    private List<Long> threadPositions = new ArrayList<Long>();
    public final Map<Long, Boolean> blockState = new HashMap<Long, Boolean>();
    public boolean running;
    public boolean finished;
    public boolean fallback;
    public int errCode = -1;
    public long timestamp;

    public transient boolean recovered;

    private transient ArrayList<WeakReference<MissionListener>> mListeners = new ArrayList<WeakReference<MissionListener>>();
    private transient boolean mWritingToFile;

    private static final int NO_IDENTIFIER = -1;
    private long db_identifier = NO_IDENTIFIER;

    public DownloadMission() {
    }

    public DownloadMission(String name, String url, String location) {
        if (name == null) throw new NullPointerException("name is null");
        if (name.isEmpty()) throw new IllegalArgumentException("name is empty");
        if (url == null) throw new NullPointerException("url is null");
        if (url.isEmpty()) throw new IllegalArgumentException("url is empty");
        if (location == null) throw new NullPointerException("location is null");
        if (location.isEmpty()) throw new IllegalArgumentException("location is empty");
        this.url = url;
        this.name = name;
        this.location = location;
    }


    private void checkBlock(long block) {
        if (block < 0 || block >= blocks) {
            throw new IllegalArgumentException("illegal block identifier");
        }
    }

    /**
     * Check if a block is reserved
     *
     * @param block the block identifier
     * @return true if the block is reserved and false if otherwise
     */
    public boolean isBlockPreserved(long block) {
        checkBlock(block);
        return blockState.containsKey(block) ? blockState.get(block) : false;
    }

    public void preserveBlock(long block) {
        checkBlock(block);
        synchronized (blockState) {
            blockState.put(block, true);
        }
    }

    /**
     * Set the download position of the file
     *
     * @param threadId the identifier of the thread
     * @param position the download position of the thread
     */
    public void setPosition(int threadId, long position) {
        threadPositions.set(threadId, position);
    }

    /**
     * Get the position of a thread
     *
     * @param threadId the identifier of the thread
     * @return the position for the thread
     */
    public long getPosition(int threadId) {
        return threadPositions.get(threadId);
    }

    public synchronized void notifyProgress(long deltaLen) {
        if (!running) return;

        if (recovered) {
            recovered = false;
        }

        done += deltaLen;

        if (done > length) {
            done = length;
        }

        if (done != length) {
            writeThisToFile();
        }

        for (WeakReference<MissionListener> ref : mListeners) {
            final MissionListener listener = ref.get();
            if (listener != null) {
                MissionListener.handlerStore.get(listener).post(new Runnable() {
                    @Override
                    public void run() {
                        listener.onProgressUpdate(DownloadMission.this, done, length);
                    }
                });
            }
        }
    }

    /**
     * Called by a download thread when it finished.
     */
    public synchronized void notifyFinished() {
        if (errCode > 0) return;

        finishCount++;

        if (finishCount == threadCount) {
            onFinish();
        }
    }

    /**
     * Called when all parts are downloaded
     */
    private void onFinish() {
        if (errCode > 0) return;

        if (DEBUG) {
            Log.d(TAG, "onFinish");
        }

        running = false;
        finished = true;

        deleteThisFromFile();

        for (WeakReference<MissionListener> ref : mListeners) {
            final MissionListener listener = ref.get();
            if (listener != null) {
                MissionListener.handlerStore.get(listener).post(new Runnable() {
                    @Override
                    public void run() {
                        listener.onFinish(DownloadMission.this);
                    }
                });
            }
        }
    }

    public synchronized void notifyError(int err) {
        errCode = err;

        writeThisToFile();

        for (WeakReference<MissionListener> ref : mListeners) {
            final MissionListener listener = ref.get();
            MissionListener.handlerStore.get(listener).post(new Runnable() {
                @Override
                public void run() {
                    listener.onError(DownloadMission.this, errCode);
                }
            });
        }
    }

    public synchronized void addListener(MissionListener listener) {
        Handler handler = new Handler(Looper.getMainLooper());
        MissionListener.handlerStore.put(listener, handler);
        mListeners.add(new WeakReference<MissionListener>(listener));
    }

    public synchronized void removeListener(MissionListener listener) {
        for (Iterator<WeakReference<MissionListener>> iterator = mListeners.iterator();
             iterator.hasNext(); ) {
            WeakReference<MissionListener> weakRef = iterator.next();
            if (listener != null && listener == weakRef.get()) {
                iterator.remove();
            }
        }
    }

    /**
     * Start downloading with multiple threads.
     */
    public void start() {
        if (!running && !finished) {
            running = true;

            if (!fallback) {
                for (int i = 0; i < threadCount; i++) {
                    if (threadPositions.size() <= i && !recovered) {
                        threadPositions.add((long) i);
                    }
                    new Thread(new DownloadRunnable(this, i)).start();
                }
            } else {
                // In fallback mode, resuming is not supported.
                threadCount = 1;
                done = 0;
                blocks = 0;
                new Thread(new DownloadRunnableFallback(this)).start();
            }
        }
    }

    public void pause() {
        if (running) {
            running = false;
            recovered = true;

            // TODO: Notify & Write state to info file
            // if (err)
        }
    }

    /**
     * Removes the file and the meta file
     */
    public void delete() {
        deleteThisFromFile();
        new File(location, name).delete();
    }

    /**
     * Write this {@link DownloadMission} to the meta file asynchronously
     * if no thread is already running.
     */
    public void writeThisToFile() {
        if (!mWritingToFile) {
            mWritingToFile = true;
            new Thread() {
                @Override
                public void run() {
                    doWriteThisToFile();
                    mWritingToFile = false;
                }
            }.start();
        }
    }

    /**
     * Write this {@link DownloadMission} to the meta file.
     */
    private void doWriteThisToFile() {
        synchronized (blockState) {
            Utility.writeToFile(getMetaFilename(), new Gson().toJson(this));
        }
    }

    private void deleteThisFromFile() {
        new File(getMetaFilename()).delete();
    }

    /**
     * Get the path of the meta file
     *
     * @return the path to the meta file
     */
    private String getMetaFilename() {
        return location + "/" + name + ".giga";
    }

    public File getDownloadedFile() {
        return new File(location, name);
    }

}
