package pipe.controllers;

import com.google.common.collect.Sets;
import pipe.controllers.application.PipeApplicationController;
import pipe.historyActions.AnimationHistory;
import pipe.utilities.gui.GuiUtils;
import uk.ac.imperial.pipe.animation.Animator;
import uk.ac.imperial.pipe.models.petrinet.Transition;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashSet;
import java.util.Set;


/**
 * This class is used to process clicks by the user to manually step
 * through enabled transitions in the net.
 */
public class GUIAnimator {

    /**
     * Timer used for spacing between random transition firings
     */
    private final Timer timer = new Timer(0, new TimedTransitionActionListener());

    /**
     * Petri net animator
     */
    private final Animator animator;

    /**
     * Petri net animation history which is responsible
     * for displaying the fired transitions and stepping forwards/backwards
     */
    private final AnimationHistory animationHistory;

    /**
     * Main PIPE application controller
     */
    private final PipeApplicationController applicationController;

    /**
     * Number of transitions fired in the current sequence
     */
    private int numberSequences = 0;

    /**
     * Constructor
     * @param animator Petri net animator
     * @param animationHistory History for animation
     * @param applicationController Pipe main application controller
     */
    public GUIAnimator(Animator animator, AnimationHistory animationHistory,
                       PipeApplicationController applicationController) {
        this.animator = animator;
        this.animationHistory = animationHistory;
        this.applicationController = applicationController;
    }

    /**
     * Saves the current petri net token counts for restoring later.
     * When exit animation mode we expect the petri net to return to
     * it's original state, hence this method must be called before animating.
     * <p>
     * Also marks the enabled transitions in the petri net.
     * </p>
     */
    public void startAnimation() {
        saveCurrentTokenState();
        markEnabledTransitions(new HashSet<Transition>(), animator.getEnabledTransitions());
    }

    /**
     * Saves the current tokens in places
     */
    private void saveCurrentTokenState() {
        animator.saveState();
    }

    /**
     * Computes transitions which need to be disabled because they are no longer enabled and
     * those that need to be enabled because they have been newly enabled.
     */
    private void markEnabledTransitions(Set<Transition> previouslyEnabled, Set<Transition> enabled) {
        for (Transition transition : Sets.difference(previouslyEnabled, enabled)) {
            transition.disable();
        }

        for (Transition transition : Sets.difference(enabled, previouslyEnabled)) {
            transition.enable();
        }
    }

    /**
     * Starts a random firing sequence for the specified number of transitions
     */
    public void startRandomFiring() {
        animationHistory.clearStepsForward();
        if (getNumberSequences() > 0) {
            // stop animation
            setNumberSequences(0);
        } else {
            try {
                String s = JOptionPane.showInputDialog("Enter number of firings to perform", "1");
                this.numberSequences = Integer.parseInt(s);
                s = JOptionPane.showInputDialog("Enter time delay between firing /ms", "50");
                timer.setDelay(Integer.parseInt(s));
                timer.start();
            } catch (NumberFormatException e) {
                GuiUtils.displayErrorMessage(null, "Error in animator: " + e.getMessage());
            }
        }
    }

    /**
     *
     * @return the number of transitions in the sequence
     */
    public synchronized int getNumberSequences() {
        return numberSequences;
    }

    /**
     *
     * @param numberSequences set the number of transitions in the sequene
     */
    public synchronized void setNumberSequences(int numberSequences) {
        this.numberSequences = numberSequences;
    }

    /**
     * Randomly fires one of the enabled transitions.
     */
    public void doRandomFiring() {
        Transition transition = animator.getRandomEnabledTransition();
        fireTransition(transition);
    }

    /**
     * This method keeps track of a fired transition in the AnimationHistoryView
     * object, enables transitions after the recent firing, and properly displays
     * the transitions.
     *
     * @param transition to be fired 
     */
    public void fireTransition(Transition transition) {
        Set<Transition> previouslyEnabled = animator.getEnabledTransitions();
        animationHistory.clearStepsForward();
        animationHistory.addHistoryItem(transition);
        animator.fireTransition(transition);

        Set<Transition> enabled = animator.getEnabledTransitions();
        markEnabledTransitions(previouslyEnabled, enabled);

    }

    /**
     * Steps back through previously fired transitions
     */
    public void stepBack() {
        if (animationHistory.isStepBackAllowed()) {
            Transition transition = animationHistory.getCurrentTransition();
            animationHistory.stepBackwards();
            animator.fireTransitionBackwards(transition);

        }
    }

    /**
     * Steps forward through previously fired transitions
     */
    public void stepForward() {
        if (isStepForwardAllowed()) {
            int nextPosition = animationHistory.getCurrentPosition() + 1;
            Transition transition = animationHistory.getTransition(nextPosition);
            animator.fireTransition(transition);
            animationHistory.stepForward();
        }
    }

    /**
     *
     * @return true if a step forward can happen in the animation history
     */
    public boolean isStepForwardAllowed() {
        return animationHistory.isStepForwardAllowed();
    }

    /**
     *
     * @return true if a step backward can happen in the animation history
     */
    public boolean isStepBackAllowed() {
        return animationHistory.isStepBackAllowed();
    }

    /**
     * Finishes the animation
     * Resets the petri net state to before animation
     */
    public void finish() {
        restoreModel();
        animationHistory.clear();
    }

    /**
     * Restores all places to their original token counts.
     * Disables all transitions
     */
    private void restoreModel() {
        animator.reset();
        for (Transition transition : animator.getEnabledTransitions()) {
            transition.disable();
        }
    }

    /**
     * Listens for the timer to run down to 0 and then performs the action
     */
    private class TimedTransitionActionListener implements ActionListener {
        /**
         * When the timer runs down to zero this will be triggered.
         *
         * It performs a random firing of a single transition
         * @param actionEvent
         */
        @Override
        public void actionPerformed(ActionEvent actionEvent) {
            PetriNetController controller = applicationController.getActivePetriNetController();
            if (getNumberSequences() < 1 || !controller.isInAnimationMode()) {
                timer.stop();
                return;
            }
            doRandomFiring();
            setNumberSequences(getNumberSequences() - 1);
        }
    }
}
