package leigh.m3;

import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.concurrent.LinkedTransferQueue;

/**
 * Implementation of a media stream. This tracks the position in the stream over time and provides methods
 * for getting access to the current window.
 *
 * @author Alex Leigh
 */
public class MediaStream {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(MediaStream.class);
    private final LinkedTransferQueue<M3U> queue = new LinkedTransferQueue<>();
    private final MediaRepository repo;
    private final ArrayList<SegmentSchedule> discos = new ArrayList<>();
    private ArrayList<SegmentSchedule> currentSegments = new ArrayList<>();
    private M3U currentPlaying = null;
    private Instant lastScheduledTime = Instant.now();
    private int globalSeq = 0;

    public MediaStream(MediaRepository repo) {
        this.repo = repo;
    }

    /**
     * Take the next media track from the source queue, or, if the source queue is empty, repopulate it
     * from the media source.
     */
    private M3U takeOrReload() throws InterruptedException {
        if (queue.isEmpty()) {
            queue.addAll(repo.getFiles());
        }
        return queue.take();
    }

    /**
     * Count the number of discontinuities that have ever occured on the stream.
     */
    private int countDiscos(Instant now) {
        int cnt = 0;

        for (SegmentSchedule disco : discos) {
            if (disco.getExpire().isBefore(now)) {
                cnt++;
            } else {
                // This relies on the premise that the list is chrono-ordered
                break;
            }
        }

        return cnt;
    }

    public ArrayList<SegmentSchedule> removeExpired(ArrayList<SegmentSchedule> list, Instant time) {
        ArrayList<SegmentSchedule> newList = new ArrayList<>();
        for (SegmentSchedule sched : list) {
            if (sched.getExpire().isBefore(time)) continue;
            newList.add(sched);
        }
        return newList;
    }

    /**
     * Return the chunks which should currently be available to the client. If necessary this will remove one or two
     * media files from the pending queue. It is this information which it used to generate the current playlists
     * M3U8 file which the clients poll.
     */
    public synchronized StreamWindow getCurrentSegments() throws InterruptedException {
        final ArrayList<SegmentSchedule> segmentSchedules = new ArrayList<>();
        final Instant now = Instant.now();

        // The number of segments to present in the stream window at any one moment
        int C = 5;

        currentSegments = removeExpired(currentSegments, now);

        // First we check to see if we have enough current segments to fill our need

        while (currentSegments.size() < C) {
            // We do not have enough to fill the current need. So drain a new media file off the queue and fill
            // its segments into currentSegments. Update lastScheduledTime and the global sequence.

            currentPlaying = takeOrReload();
            logger.info("Now playing. [media: {}]", currentPlaying);

            int cnt = 0;
            for (MediaSegment chunk : currentPlaying.getSegments()) {
                Instant expire = lastScheduledTime.plus(chunk.getDurationMs(), ChronoUnit.MILLIS);
                logger.debug("New expire: {} [global: {}]", expire, globalSeq);
                SegmentSchedule sched = new SegmentSchedule(chunk, expire, globalSeq, cnt == 0);

                if (cnt == 0) {
                    discos.add(sched);
                }

                currentSegments.add(new SegmentSchedule(chunk, expire, globalSeq, cnt == 0));
                globalSeq++;
                cnt++;
                lastScheduledTime = expire;
            }
        }

        // We are now sure that we have enough segments to populate the need.
        for (SegmentSchedule sched : currentSegments) {
            segmentSchedules.add(sched);
            if (segmentSchedules.size() == C) return new StreamWindow(countDiscos(now), segmentSchedules);
        }

        // We should never get here.
        logger.error("Unreachable case reached.");

        return new StreamWindow(countDiscos(now), segmentSchedules);
    }
}

