package ctbrec.recorder.download.hls;

import static ctbrec.io.HttpConstants.*;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.xml.bind.JAXBException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.EncryptionData;
import com.iheartradio.m3u8.data.MediaPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.TrackData;

import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording.State;
import ctbrec.UnknownModel;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException;
import ctbrec.recorder.download.AbstractDownload;
import ctbrec.recorder.download.StreamSource;
import ctbrec.sites.Site;
import okhttp3.Request;
import okhttp3.Response;

public abstract class AbstractHlsDownload extends AbstractDownload {

    private static final Logger LOG = LoggerFactory.getLogger(AbstractHlsDownload.class);
    private static int threadCounter = 0;

    protected transient HttpClient client;
    protected volatile boolean running = false;
    protected Model model = new UnknownModel();
    protected transient LinkedBlockingQueue<Runnable> downloadQueue = new LinkedBlockingQueue<>(50);
    protected transient ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue, createThreadFactory());
    protected State state = State.UNKNOWN;
    private int playlistEmptyCount = 0;

    public AbstractHlsDownload(HttpClient client) {
        this.client = client;
    }

    private ThreadFactory createThreadFactory() {
        return r -> {
            Thread t = new Thread(r);
            t.setDaemon(true);
            t.setName("SegmentDownloadThread-" + threadCounter++);
            return t;
        };
    }

    protected SegmentPlaylist getNextSegments(String segmentsURL) throws IOException, ParseException, PlaylistException {
        URL segmentsUrl = new URL(segmentsURL);
        Request request = new Request.Builder()
                .url(segmentsUrl)
                .header(ACCEPT, "*/*")
                .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
                .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
                .header(CONNECTION, KEEP_ALIVE)
                .header(ORIGIN, Optional.ofNullable(model).map(Model::getSite).map(Site::getBaseUrl).orElse(""))
                .header(REFERER, Optional.ofNullable(model).map(Model::getSite).map(Site::getBaseUrl).orElse(""))
                .build();

        try (Response response = client.execute(request)) {
            if (response.isSuccessful()) {
                String body = response.body().string();
                if (!body.contains("#EXTINF")) {
                    // no segments, empty playlist
                    return new SegmentPlaylist(segmentsURL);
                }

                InputStream inputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
                PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
                Playlist playlist = parser.parse();
                if (playlist.hasMediaPlaylist()) {
                    MediaPlaylist mediaPlaylist = playlist.getMediaPlaylist();
                    SegmentPlaylist lsp = new SegmentPlaylist(segmentsURL);
                    lsp.seq = mediaPlaylist.getMediaSequenceNumber();
                    lsp.targetDuration = mediaPlaylist.getTargetDuration();
                    List<TrackData> tracks = mediaPlaylist.getTracks();
                    for (TrackData trackData : tracks) {
                        String uri = trackData.getUri();
                        if (!uri.startsWith("http")) {
                            String tmpurl = segmentsUrl.toString();
                            tmpurl = tmpurl.substring(0, tmpurl.lastIndexOf('/') + 1);
                            uri = tmpurl + uri;
                        }
                        lsp.totalDuration += trackData.getTrackInfo().duration;
                        lsp.lastSegDuration = trackData.getTrackInfo().duration;
                        lsp.segments.add(uri);
                        if (trackData.hasEncryptionData()) {
                            lsp.encrypted = true;
                            EncryptionData data = trackData.getEncryptionData();
                            lsp.encryptionKeyUrl = data.getUri();
                            lsp.encryptionMethod = data.getMethod().getValue();
                        }
                    }
                    return lsp;
                }
                throw new InvalidPlaylistException("Playlist has no media playlist");
            } else {
                throw new HttpException(response.code(), response.message());
            }
        }
    }


    protected String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
        LOG.debug("{} stream idx: {}", model.getName(), model.getStreamUrlIndex());
        List<StreamSource> streamSources = model.getStreamSources();
        Collections.sort(streamSources);
        for (StreamSource streamSource : streamSources) {
            LOG.debug("{} src {}", model.getName(), streamSource);
        }
        String url = null;
        if (model.getStreamUrlIndex() >= 0 && model.getStreamUrlIndex() < streamSources.size()) {
            // TODO don't use the index, but the bandwidth. if the bandwidth does not match, take the closest one
            LOG.debug("{} selected {}", model.getName(), streamSources.get(model.getStreamUrlIndex()));
            url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl();
        } else {
            // filter out stream resolutions, which are too high
            int maxRes = Config.getInstance().getSettings().maximumResolution;
            if (maxRes > 0) {
                for (Iterator<StreamSource> iterator = streamSources.iterator(); iterator.hasNext();) {
                    StreamSource streamSource = iterator.next();
                    if (streamSource.height > 0 && maxRes < streamSource.height) {
                        LOG.trace("Res too high {} > {}", streamSource.height, maxRes);
                        iterator.remove();
                    }
                }
            }
            if (streamSources.isEmpty()) {
                throw new ExecutionException(new RuntimeException("No stream left in playlist"));
            } else {
                LOG.debug("{} selected {}", model.getName(), streamSources.get(streamSources.size() - 1));
                url = streamSources.get(streamSources.size() - 1).getMediaPlaylistUrl();
            }
        }
        LOG.debug("Segment playlist url {}", url);
        return url;
    }

    protected void emptyPlaylistCheck(SegmentPlaylist playlist) {
        if(playlist.segments.isEmpty()) {
            playlistEmptyCount++;
            try {
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        } else {
            playlistEmptyCount = 0;
        }
        if(playlistEmptyCount == 10) {
            LOG.info("Last 10 playlists were empty for {}. Stopping recording!", getModel());
            internalStop();
        }
    }

    abstract void internalStop();

    @Override
    public Model getModel() {
        return model;
    }

    /**
     * Causes the current thread to sleep for a short amount of time.
     * This is used to slow down retries, if something is wrong with the playlist.
     * E.g. HTTP 403 or 404
     */
    protected void waitSomeTime(long waitForMillis) {
        try {
            Thread.sleep(waitForMillis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            if(running) {
                LOG.error("Couldn't sleep. This might mess up the download!");
            }
        }
    }

    public static class SegmentPlaylist {
        public String url;
        public int seq = 0;
        public float totalDuration = 0;
        public float lastSegDuration = 0;
        public float targetDuration = 0;
        public List<String> segments = new ArrayList<>();
        public boolean encrypted = false;
        public String encryptionMethod = "AES-128";
        public String encryptionKeyUrl;

        public SegmentPlaylist(String url) {
            this.url = url;
        }
    }
}
