package us.shandian.giga.get;

import android.util.Log;

import org.schabi.newpipe.streams.io.SharpStream;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
import java.util.Objects;

import us.shandian.giga.get.DownloadMission.Block;
import us.shandian.giga.get.DownloadMission.HttpError;

import static org.schabi.newpipe.BuildConfig.DEBUG;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_AUTH;
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;


/**
 * Runnable to download blocks of a file until the file is completely downloaded,
 * an error occurs or the process is stopped.
 */
public class DownloadRunnable extends Thread {
    private static final String TAG = "DownloadRunnable";

    private final DownloadMission mMission;
    private final int mId;

    private HttpURLConnection mConn;

    DownloadRunnable(DownloadMission mission, int id) {
        mMission = Objects.requireNonNull(mission);
        mId = id;
    }

    private void releaseBlock(Block block, long remain) {
        // set the block offset to -1 if it is completed
        mMission.releaseBlock(block.position, remain < 0 ? -1 : block.done);
    }

    @Override
    public void run() {
        boolean retry = false;
        Block block = null;
        int retryCount = 0;
        SharpStream f;

        try {
            f = mMission.storage.getStream();
        } catch (IOException e) {
            mMission.notifyError(e);// this never should happen
            return;
        }

        while (mMission.running && mMission.errCode == DownloadMission.ERROR_NOTHING) {
            if (!retry) {
                block = mMission.acquireBlock();
            }

            if (block == null) {
                if (DEBUG) Log.d(TAG, mId + ":no more blocks left, exiting");
                break;
            }

            if (DEBUG) {
                if (retry)
                    Log.d(TAG, mId + ":retry block at position=" + block.position + " from the start");
                else
                    Log.d(TAG, mId + ":acquired block at position=" + block.position + " done=" + block.done);
            }

            long start = (long)block.position * DownloadMission.BLOCK_SIZE;
            long end = start + DownloadMission.BLOCK_SIZE - 1;

            start += block.done;

            if (end >= mMission.length) {
                end = mMission.length - 1;
            }

            try {
                mConn = mMission.openConnection(false, start, end);
                mMission.establishConnection(mId, mConn);

                // check if the download can be resumed
                if (mConn.getResponseCode() == 416) {
                    if (block.done > 0) {
                        // try again from the start (of the block)
                        mMission.notifyProgress(-block.done);
                        block.done = 0;
                        retry = true;
                        mConn.disconnect();
                        continue;
                    }

                    throw new DownloadMission.HttpError(416);
                }

                retry = false;

                // The server may be ignoring the range request
                if (mConn.getResponseCode() != 206) {
                    if (DEBUG) {
                        Log.e(TAG, mId + ":Unsupported " + mConn.getResponseCode());
                    }
                    mMission.notifyError(new DownloadMission.HttpError(mConn.getResponseCode()));
                    break;
                }

                f.seek(mMission.offsets[mMission.current] + start);

                try (InputStream is = mConn.getInputStream()) {
                    byte[] buf = new byte[DownloadMission.BUFFER_SIZE];
                    int len;

                    // use always start <= end
                    // fixes a deadlock because in some videos, youtube is sending one byte alone
                    while (start <= end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) {
                        f.write(buf, 0, len);
                        start += len;
                        block.done += len;
                        mMission.notifyProgress(len);
                    }
                }

                if (DEBUG && mMission.running) {
                    Log.d(TAG, mId + ":position " + block.position + " stopped " + start + "/" + end);
                }
            } catch (Exception e) {
                if (!mMission.running || e instanceof ClosedByInterruptException) break;

                if (e instanceof HttpError && (((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN || ((HttpError) e).statusCode == ERROR_HTTP_AUTH)) {
                    // for youtube streams. The url has expired, recover
                    f.close();

                    if (mId == 1) {
                        // only the first thread will execute the recovery procedure
                        mMission.doRecover(ERROR_HTTP_FORBIDDEN);
                    }
                    return;
                }

                if (retryCount++ >= mMission.maxRetry) {
                    mMission.notifyError(e);
                    break;
                }

                retry = true;
            } finally {
                if (!retry) releaseBlock(block, end - start);
            }
        }

        f.close();

        if (DEBUG) {
            Log.d(TAG, "thread " + mId + " exited from main download loop");
        }

        if (mMission.errCode == DownloadMission.ERROR_NOTHING && mMission.running) {
            if (DEBUG) {
                Log.d(TAG, "no error has happened, notifying");
            }
            mMission.notifyFinished();
        }

        if (DEBUG && !mMission.running) {
            Log.d(TAG, "The mission has been paused. Passing.");
        }
    }

    @Override
    public void interrupt() {
        super.interrupt();

        try {
            if (mConn != null) mConn.disconnect();
        } catch (Exception e) {
            // nothing to do
        }
    }

}
