package ctbrec.recorder.download;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.BlockingQueue;
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 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.OS;
import ctbrec.Recording;
import ctbrec.Recording.State;
import ctbrec.UnknownModel;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.io.StreamRedirectThread;
import okhttp3.Request;
import okhttp3.Response;

public abstract class AbstractHlsDownload implements Download {

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

    protected HttpClient client;
    protected volatile boolean running = false;
    protected Instant startTime;
    protected Model model = new UnknownModel();
    protected BlockingQueue<Runnable> downloadQueue = new LinkedBlockingQueue<>(50);
    protected 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", "en-US,en;q=0.5")
                .header("User-Agent", Config.getInstance().getSettings().httpUserAgent)
                .header("Origin", model.getSite().getBaseUrl())
                .header("Referer", model.getSite().getBaseUrl())
                .header("Connection", "keep-alive")
                .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("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 _url = segmentsUrl.toString();
                            _url = _url.substring(0, _url.lastIndexOf('/') + 1);
                            uri = _url + 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;
                }
                return null;
            } else {
                throw new HttpException(response.code(), response.message());
            }
        }
    }


    protected String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException {
        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) {
            }
        } else {
            playlistEmptyCount = 0;
        }
        if(playlistEmptyCount == 10) {
            LOG.info("Last 10 playlists were empty for {}. Stopping recording!", getModel());
            internalStop();
        }
    }

    abstract void internalStop();

    @Override
    public Instant getStartTime() {
        return startTime;
    }

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

    @Override
    public void postprocess(Recording recording) {
        runPostProcessingScript(recording);
    }

    private void runPostProcessingScript(Recording recording) {
        String postProcessing = Config.getInstance().getSettings().postProcessing;
        if (postProcessing != null && !postProcessing.isEmpty()) {
            File target = recording.getAbsoluteFile();
            Runtime rt = Runtime.getRuntime();
            try {
                String[] args = new String[] {
                        postProcessing,
                        target.getParentFile().getAbsolutePath(),
                        target.getAbsolutePath(),
                        getModel().getName(),
                        getModel().getSite().getName(),
                        Long.toString(recording.getStartDate().getEpochSecond())
                };
                LOG.debug("Running {}", Arrays.toString(args));
                Process process = rt.exec(args, OS.getEnvironment());
                // TODO maybe write these to a separate log file, e.g. recname.ts.pp.log
                Thread std = new Thread(new StreamRedirectThread(process.getInputStream(), System.out));
                std.setName("Process stdout pipe");
                std.setDaemon(true);
                std.start();
                Thread err = new Thread(new StreamRedirectThread(process.getErrorStream(), System.err));
                err.setName("Process stderr pipe");
                err.setDaemon(true);
                err.start();

                process.waitFor();
                LOG.debug("Process finished.");
            } catch (Exception e) {
                LOG.error("Error in process thread", e);
            }
        }
    }

    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;
        }
    }
}
