package skadistats.clarity.processor.runner;

import com.google.protobuf.GeneratedMessage;
import skadistats.clarity.ClarityException;
import skadistats.clarity.processor.reader.PacketInstance;
import skadistats.clarity.source.PacketPosition;
import skadistats.clarity.source.ResetRelevantKind;
import skadistats.clarity.source.Source;

import java.io.EOFException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.TreeSet;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ControllableRunner extends AbstractFileRunner {

    private final ReentrantLock lock = new ReentrantLock();
    private final Condition wantedTickReached = lock.newCondition();
    private final Condition moreProcessingNeeded = lock.newCondition();

    private Thread runnerThread;
    private Exception runnerException;

    private TreeSet<PacketPosition> resetRelevantPackets = new TreeSet<>();
    private int resetRelevantOffset = -1;
    private LinkedList<ResetStep> resetSteps;

    /* tick the processor is waiting at to be signaled to continue further processing */
    private int upcomingTick;
    /* tick we want to be at the end of */
    private int wantedTick;
    /* tick the user wanted to be at the end of */
    private Integer demandedTick;

    private long t0;

    private final LoopController.Func normalLoopControl = new LoopController.Func() {
        @Override
        public LoopController.Command doLoopControl(int nextTickWithData) throws Exception {
            if (!loopController.isSyncTickSeen()) {
                if (tick == -1) {
                    wantedTick = 0;
                    startNewTick(0);
                }
                return LoopController.Command.FALLTHROUGH;
            }
            upcomingTick = nextTickWithData;
            if (upcomingTick == tick) {
                return LoopController.Command.FALLTHROUGH;
            }
            if (demandedTick != null) {
                handleDemandedTick();
                return LoopController.Command.AGAIN;
            }
            endTicksUntil(tick);
            if (tick == wantedTick) {
                if (log.isDebugEnabled() && t0 != 0) {
                    log.debug("now at %d. Took %d microns.", tick, (System.nanoTime() - t0) / 1000);
                    t0 = 0;
                }
                wantedTickReached.signalAll();
                moreProcessingNeeded.await();
                if (demandedTick != null) {
                    handleDemandedTick();
                    return LoopController.Command.AGAIN;
                }
            }
            startNewTick(upcomingTick);
            if (wantedTick < upcomingTick) {
                return LoopController.Command.AGAIN;
            }
            return LoopController.Command.FALLTHROUGH;
        }

        private void handleDemandedTick() throws IOException {
            wantedTick = demandedTick;
            demandedTick = null;
            int diff = wantedTick - tick;

            if (diff >= 0 && diff <= 5) {
                return;
            }

            resetSteps = new LinkedList<>();
            resetSteps.add(new ResetStep(LoopController.Command.RESET_START, null));
            if (diff < 0 || engineType.isFullPacketSeekAllowed()) {
                TreeSet<PacketPosition> seekPositions = getResetPacketsBeforeTick(wantedTick);
                resetSteps.add(new ResetStep(LoopController.Command.RESET_CLEAR, null));
                while (seekPositions.size() > 0) {
                    PacketPosition pp = seekPositions.pollFirst();
                    switch (pp.getKind()) {
                        case STRINGTABLE:
                        case FULL_PACKET:
                            resetSteps.add(new ResetStep(LoopController.Command.CONTINUE, pp.getOffset()));
                            resetSteps.add(new ResetStep(LoopController.Command.RESET_ACCUMULATE, null));
                            if (seekPositions.size() == 0) {
                                resetSteps.add(new ResetStep(LoopController.Command.RESET_APPLY, null));
                            }
                            break;
                        case SYNC:
                            if (seekPositions.size() == 0) {
                                resetSteps.add(new ResetStep(LoopController.Command.CONTINUE, pp.getOffset()));
                                resetSteps.add(new ResetStep(LoopController.Command.RESET_APPLY, null));
                            }
                            break;
                    }
                }
            }
            resetSteps.add(new ResetStep(LoopController.Command.RESET_FORWARD, null));
            resetSteps.add(new ResetStep(LoopController.Command.RESET_COMPLETE, null));
            loopController.controllerFunc = seekLoopControl;
        }
    };

    private final LoopController.Func seekLoopControl = new LoopController.Func() {
        @Override
        public LoopController.Command doLoopControl(int nextTickWithData) throws Exception {
            upcomingTick = nextTickWithData;
            ResetStep step = resetSteps.peekFirst();
            switch (step.command) {
                case CONTINUE:
                    resetSteps.pollFirst();
                    source.setPosition(step.offset);
                    return step.command;
                case RESET_FORWARD:
                    if (wantedTick >= upcomingTick) {
                        return LoopController.Command.FALLTHROUGH;
                    }
                    resetSteps.pollFirst();
                    return LoopController.Command.AGAIN;
                case RESET_COMPLETE:
                    resetSteps = null;
                    loopController.controllerFunc = normalLoopControl;
                    tick = wantedTick - 1;
                    startNewTick(upcomingTick);
                    return LoopController.Command.RESET_COMPLETE;
                default:
                    resetSteps.pollFirst();
                    return step.command;
            }
        }
    };

    public class LockingLoopController extends LoopController {

        public LockingLoopController(Func controllerFunc) {
            super(controllerFunc);
        }

        @Override
        public Command doLoopControl(int nextTick) throws Exception {
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                return Command.BREAK;
            }
            try {
                return super.doLoopControl(nextTick);
            } finally {
                lock.unlock();
            }
        }

        @Override
        public void markResetRelevantPacket(int tick, ResetRelevantKind kind, int offset) throws IOException {
            lock.lock();
            try {
                PacketPosition pp = newResetRelevantPacketPosition(tick, kind, offset);
                if (pp == null) {
                    throw new ClarityException("tried to mark non reset relevant packet");
                }
                addResetRelevant(pp);
            } finally {
                lock.unlock();
            }
        }
    }

    private void addResetRelevant(PacketPosition pp) {
        if (pp.getOffset() > resetRelevantOffset) {
            resetRelevantPackets.add(pp);
            resetRelevantOffset = pp.getOffset();
        }
    }

    private PacketPosition newResetRelevantPacketPosition(int tick, ResetRelevantKind kind, int offset) {
        return kind == null ? null : PacketPosition.createPacketPosition(loopController.isSyncTickSeen() ? tick : -1, kind, offset);
    }

    private TreeSet<PacketPosition> getResetPacketsBeforeTick(int wantedTick) throws IOException {
        int backup = source.getPosition();
        PacketPosition wanted = PacketPosition.createPacketPosition(wantedTick, ResetRelevantKind.FULL_PACKET, 0);
        if (resetRelevantPackets.tailSet(wanted, true).size() == 0) {
            PacketPosition basePos = resetRelevantPackets.floor(wanted);
            source.setPosition(basePos.getOffset());
            int lastFullFoundAtTick = basePos.getTick();
            try {
                while (true) {
                    int at = source.getPosition();
                    PacketInstance<GeneratedMessage> pi = engineType.getNextPacketInstance(source);
                    PacketPosition pp = newResetRelevantPacketPosition(pi.getTick(), pi.getResetRelevantKind(), at);
                    if (pp != null) {
                        lastFullFoundAtTick = pp.getTick();
                        addResetRelevant(pp);
                    }
                    if (pi.getTick() >= wantedTick) {
                        break;
                    }
                    if (pi.getTick() >= lastFullFoundAtTick + engineType.getExpectedFullPacketInterval() + 100) {
                        break;
                    }
                    pi.skip();
                }
            } catch (EOFException e) {
            }
        }
        source.setPosition(backup);
        return new TreeSet<>(resetRelevantPackets.headSet(wanted, true));
    }

    public static class ResetStep {
        private final LoopController.Command command;
        private final Integer offset;
        public ResetStep(LoopController.Command command, Integer offset) {
            this.command = command;
            this.offset = offset;
        }
    }

    public ControllableRunner(Source s) throws IOException {
        super(s, s.readEngineType());
        upcomingTick = tick;
        wantedTick = tick;
        this.loopController = new LockingLoopController(normalLoopControl);
    }

    public ControllableRunner runWith(final Object... processors) {
        runnerThread = new Thread(() -> {
            log.debug("runner started");
            try {
                initAndRunWith(processors);
            } catch (Exception e) {
                if (e instanceof InterruptedException) {
                    Thread.currentThread().interrupt();
                } else {
                    log.error("Runner thread crashed", e);
                }
                lock.lock();
                try {
                    runnerException = e;
                    wantedTickReached.signal();
                } finally {
                    lock.unlock();
                }
            }
            log.debug("runner finished");
            runnerThread = null;
        });
        runnerThread.setName("clarity-runner");
        runnerThread.setDaemon(false);
        runnerThread.start();
        return this;
    }

    public boolean isResetting() {
        lock.lock();
        try {
            return loopController.controllerFunc == seekLoopControl;
        } finally {
            lock.unlock();
        }
    }

    public boolean isRunning() {
        return runnerThread != null;
    }

    public void setDemandedTick(int demandedTick) {
        lock.lock();
        try {
            this.demandedTick = demandedTick;
            t0 = System.nanoTime();
            moreProcessingNeeded.signal();
        } finally {
            lock.unlock();
        }
    }

    private void waitForTickReached() throws InterruptedException {
        wantedTickReached.await();
        if (runnerException != null) {
            throw new InterruptedException() {
                @Override
                public String getMessage() {
                    return "Runner thread was terminated by an exception";
                }
                @Override
                public synchronized Throwable getCause() {
                    return runnerException;
                }
            };
        }
    }

    public void seek(int demandedTick) throws InterruptedException {
        lock.lock();
        try {
            this.demandedTick = demandedTick;
            t0 = System.nanoTime();
            moreProcessingNeeded.signal();
            waitForTickReached();
        } finally {
            lock.unlock();
        }
    }

    public void tick() throws InterruptedException {
        lock.lock();
        try {
            if (tick != wantedTick) {
                waitForTickReached();
            }
            wantedTick++;
            t0 = System.nanoTime();
            moreProcessingNeeded.signal();
            waitForTickReached();
        } finally {
            lock.unlock();
        }
    }

    public boolean isAtEnd() {
        return upcomingTick == Integer.MAX_VALUE;
    }

    public void halt() {
        if (runnerThread != null && runnerThread.isAlive()) {
            runnerThread.interrupt();
        }
    }

    @Override
    public int getLastTick() {
        lock.lock();
        try {
            try {
                return source.getLastTick();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        } finally {
            lock.unlock();
        }
    }

}
