package tdl.record.sourcecode.record;

import lombok.extern.slf4j.Slf4j;
import tdl.record.sourcecode.content.SourceCodeProvider;
import tdl.record.sourcecode.metrics.SourceCodeRecordingListener;
import tdl.record.sourcecode.metrics.SourceCodeRecordingMetricsCollector;
import tdl.record.sourcecode.snapshot.SnapshotRecorderException;
import tdl.record.sourcecode.snapshot.file.Writer;
import tdl.record.sourcecode.time.SystemMonotonicTimeSource;
import tdl.record.sourcecode.time.TimeSource;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.Queue;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import static java.nio.file.StandardOpenOption.CREATE;

@Slf4j
public class SourceCodeRecorder {

    private final SourceCodeProvider sourceCodeProvider;
    private final Path outputRecordingFilePath;
    private final TimeSource timeSource;
    private final long snapshotIntervalMillis;
    private final long recordingStartTimestamp;
    private final int keySnapshotSpacing;
    private final AtomicBoolean shouldStopJob;
    private final SourceCodeRecordingListener sourceCodeRecordingListener;
    private Queue<String> tagQueue;

    SourceCodeRecorder(SourceCodeProvider sourceCodeProvider,
                       Path outputRecordingFilePath,
                       TimeSource timeSource,
                       long recordedTimestamp,
                       long snapshotIntervalMillis,
                       int keySnapshotSpacing, SourceCodeRecordingListener sourceCodeRecordingListener) {
        this.sourceCodeProvider = sourceCodeProvider;
        this.outputRecordingFilePath = outputRecordingFilePath;
        this.timeSource = timeSource;
        this.recordingStartTimestamp = recordedTimestamp;
        this.snapshotIntervalMillis = snapshotIntervalMillis;
        this.keySnapshotSpacing = keySnapshotSpacing;
        this.sourceCodeRecordingListener = sourceCodeRecordingListener;
        shouldStopJob = new AtomicBoolean(false);
        tagQueue = new ConcurrentLinkedQueue<>();
    }

    @SuppressWarnings("SameParameterValue")
    public static class Builder {

        private final SourceCodeProvider bSourceCodeProvider;
        private final Path bOutputRecordingFilePath;
        private TimeSource bTimeSource;
        private long bSnapshotIntervalMillis;
        private long bRecordingStartTimestampSec;
        private int bKeySnapshotSpacing;
        private SourceCodeRecordingListener bSourceCodeRecordingListener;

        public Builder(SourceCodeProvider sourceCodeProvider, Path outputRecordingFilePath) {
            bSourceCodeProvider = sourceCodeProvider;
            bOutputRecordingFilePath = outputRecordingFilePath;
            bRecordingStartTimestampSec = System.currentTimeMillis() / 1000;
            bTimeSource = new SystemMonotonicTimeSource();
            bSnapshotIntervalMillis = TimeUnit.MINUTES.toMillis(5);
            bKeySnapshotSpacing = 5;
            bSourceCodeRecordingListener = new SourceCodeRecordingMetricsCollector();
        }

        public Builder withRecordingStartTimestampSec(long recordingStartTimestampSec) {
            this.bRecordingStartTimestampSec = recordingStartTimestampSec;
            return this;
        }

        public Builder withTimeSource(TimeSource timeSource) {
            this.bTimeSource = timeSource;
            return this;
        }

        public Builder withSnapshotEvery(int interval, TimeUnit timeUnit) {
            this.bSnapshotIntervalMillis = timeUnit.toMillis(interval);
            return this;
        }

        public Builder withKeySnapshotSpacing(int keySnapshotSpacing) {
            this.bKeySnapshotSpacing = keySnapshotSpacing;
            return this;
        }

        public Builder withRecordingListener(SourceCodeRecordingListener sourceCodeRecordingListener) {
            this.bSourceCodeRecordingListener = sourceCodeRecordingListener;
            return this;
        }

        public SourceCodeRecorder build() {
            return new SourceCodeRecorder(
                    bSourceCodeProvider,
                    bOutputRecordingFilePath,
                    bTimeSource,
                    bRecordingStartTimestampSec,
                    bSnapshotIntervalMillis,
                    bKeySnapshotSpacing,
                    bSourceCodeRecordingListener
            );
        }
    }

    public void start(Duration recordingDuration) throws SourceCodeRecorderException {
        Writer writer;
        Path lockFilePath = Paths.get(outputRecordingFilePath + ".lock");
        try {
            Files.write(lockFilePath, new byte[0], CREATE);
            //TODO Initialise inside constructor once SnapshotsFileWriter::new is free from exceptions
            writer = new Writer(
                    outputRecordingFilePath,
                    sourceCodeProvider,
                    timeSource,
                    recordingStartTimestamp,
                    keySnapshotSpacing,
                    false
            );
        } catch (IOException | SnapshotRecorderException e) {
            throw new SourceCodeRecorderException("Failed to open destination", e);
        }

        try {
            sourceCodeRecordingListener.notifyRecordingStart(outputRecordingFilePath);
            doRecord(writer, recordingDuration);
        } finally {
            sourceCodeRecordingListener.notifyRecordingEnd();
        }
    }

    public void tagCurrentState(String tag) throws SourceCodeRecorderException {
        log.info("Tag state with: " + tag);
        tagQueue.offer(tag);
        timeSource.wakeUpNow();
    }

    private void doRecord(Writer writer, Duration recordingDuration) {
        while (timeSource.currentTimeNano() < recordingDuration.toNanos()) {
            long timestampBeforeProcessing = timeSource.currentTimeNano();
            sourceCodeRecordingListener.notifySnapshotStart(timestampBeforeProcessing, TimeUnit.NANOSECONDS);
            String tag = tagQueue.poll();
            writer.takeSnapshotWithTag(tag);
            sourceCodeRecordingListener.notifySnapshotEnd(timeSource.currentTimeNano(), TimeUnit.NANOSECONDS);

            // Allow a different thread to stop the recording
            // This operation should be before wakeUp to allow a final snapshot to be taken ( if the time allows it )
            if (shouldStopJob.get()) {
                break;
            }

            // Prepare the next timestamp
            long nextTimestamp = timestampBeforeProcessing + TimeUnit.MILLISECONDS.toNanos(snapshotIntervalMillis);
            try {
                timeSource.wakeUpAt(nextTimestamp, TimeUnit.NANOSECONDS);
            } catch (InterruptedException | BrokenBarrierException e) {
                log.debug("Interrupted while sleeping", e);
            }
        }
    }

    public void stop() {
        if (!shouldStopJob.get()) {
            log.info("Stopping recording");
            shouldStopJob.set(true);
            timeSource.wakeUpNow();
        } else {
            log.info("Recording already stopping");
        }
    }

    public void close() {
        try {
            log.info("Closing the source code stream");
            //TODO Ensure the file is finalised correctly

            //delete lock file after closing writing
            Path lockFilePath = Paths.get(outputRecordingFilePath + ".lock");
            Files.delete(lockFilePath);
        } catch (IOException e) {
            throw new RuntimeException("Can't delete *.lock file.", e);
        }
    }
}

@@ -933,24 +933,81 @@
 deRecorder %7B
+%0A    private static final int VERY_SHORT_DURATION = 1000;
 %0A%0A    privat
@@ -5732,43 +5732,8 @@
 tag)
- throws SourceCodeRecorderException
  %7B%0A 
@@ -6648,152 +6648,489 @@
     
-// Prepare the next timestamp%0A            long nextTimestam
+long timeToSleep;%0A            if (tagQueue.size() %3E 0) %7B%0A                // Wake up quickly is we need to process a new tag%0A                timeToSlee
 p = 
-timestampBeforeProcessing + TimeUnit.MILLISECONDS.toNanos(snapshotIntervalMillis)
+TimeUnit.MILLISECONDS.toNanos(VERY_SHORT_DURATION);%0A            %7D else %7B%0A                // Wake up as scheduled%0A                timeToSleep = TimeUnit.MILLISECONDS.toNanos(snapshotIntervalMillis);%0A            %7D%0A%0A            // Prepare the next timestamp%0A            long nextTimestamp = timestampBeforeProcessing + timeToSleep
 ;%0A  

Fix a tagging problem were it will take a few cycles to process one tag.