package pipe.controllers.application;

import pipe.actions.gui.PipeApplicationModel;
import pipe.controllers.*;
import pipe.gui.PetriNetTab;
import pipe.historyActions.AnimationHistoryImpl;
import uk.ac.imperial.pipe.animation.PetriNetAnimator;
import uk.ac.imperial.pipe.models.manager.PetriNetManager;
import uk.ac.imperial.pipe.models.manager.PetriNetManagerImpl;
import uk.ac.imperial.pipe.models.petrinet.*;
import uk.ac.imperial.pipe.parsers.UnparsableException;

import javax.swing.event.UndoableEditListener;
import javax.xml.bind.JAXBException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

/**
 * Pipes main application controller.
 * It houses the Petri net controllers of open tabs and is responsible for the creation of Petri nets
 */
public class PipeApplicationController {

    /**
     * Controllers for each tab
     */
    private final Map<PetriNetTab, PetriNetController> netControllers = new HashMap<>();

    /**
     * Main PIPE application model
     */
    private final PipeApplicationModel applicationModel;

    /**
     * Manages creation/deletion of Petri net models
     */
    private final PetriNetManager manager = new PetriNetManagerImpl();

    /**
     * The current tab displayed in the view
     */
    private PetriNetTab activeTab;

    /**
     * Constructor
     * @param applicationModel Main PIPE application model
     */
    public PipeApplicationController(PipeApplicationModel applicationModel) {
        this.applicationModel = applicationModel;
    }

    /**
     *
     * @param listener to listen for change events in the petri net manager
     */
    public void registerToManager(PropertyChangeListener listener) {
        manager.addPropertyChangeListener(listener);
    }

    /**
     * Creates an empty Petri net with a default token
     */
    public void createEmptyPetriNet() {
        manager.createNewPetriNet();
    }


    /**
     * Register the tab to the Petri net
     * @param net Petri net
     * @param tab tab which houses the graphical petri net components
     * @param historyObserver listener for stepback/forward events in animation
     * @param undoListener listener for undo/redo events
     * @param zoomListener listener for zoom events
     */
    //TODO: THIS IS RATHER UGLY, too many params but better than what was here before
    public void registerTab(PetriNet net, PetriNetTab tab, Observer historyObserver, UndoableEditListener undoListener,
                            PropertyChangeListener zoomListener) {
        AnimationHistoryImpl animationHistory = new AnimationHistoryImpl();
        animationHistory.addObserver(historyObserver);
        GUIAnimator animator = new GUIAnimator(new PetriNetAnimator(net), animationHistory, this);

        CopyPasteManager copyPasteManager = new CopyPasteManager(undoListener, tab, net, this);

        ZoomController zoomController = new ZoomController(100);
        tab.addZoomListener(zoomController);
        PetriNetController petriNetController =
                new PetriNetController(net, undoListener, animator, copyPasteManager, zoomController, tab);
        netControllers.put(tab, petriNetController);
        tab.updatePreferredSize();

        PropertyChangeListener changeListener =
                new PetriNetChangeListener(applicationModel, tab, petriNetController);
        net.addPropertyChangeListener(changeListener);

        setActiveTab(tab);
        initialiseNet(net, changeListener);
    }

    /**
     *
     * @param tab the active tab - this is the tab that is currently being displayed in the view
     */
    public void setActiveTab(PetriNetTab tab) {
        this.activeTab = tab;
    }

    /**
     * This is a little hacky, I'm not sure how to make this better when it's so late
     * If a better implementation is clear please re-write
     * <p/>
     * This method invokes the change listener which will create the view objects on the
     * petri net tab
     *
     * @param propertyChangeListener
     */
    private void initialiseNet(PetriNet net, PropertyChangeListener propertyChangeListener) {
        for (Token token : net.getTokens()) {
            PropertyChangeEvent changeEvent =
                    new PropertyChangeEvent(net, PetriNet.NEW_TOKEN_CHANGE_MESSAGE, null, token);
            propertyChangeListener.propertyChange(changeEvent);
        }

        for (Place place : net.getPlaces()) {
            PropertyChangeEvent changeEvent =
                    new PropertyChangeEvent(net, PetriNet.NEW_PLACE_CHANGE_MESSAGE, null, place);
            propertyChangeListener.propertyChange(changeEvent);
        }

        for (Transition transition : net.getTransitions()) {
            PropertyChangeEvent changeEvent =
                    new PropertyChangeEvent(net, PetriNet.NEW_TRANSITION_CHANGE_MESSAGE, null, transition);
            propertyChangeListener.propertyChange(changeEvent);
        }

        for (Arc<? extends Connectable, ? extends Connectable> arc : net.getArcs()) {
            PropertyChangeEvent changeEvent = new PropertyChangeEvent(net, PetriNet.NEW_ARC_CHANGE_MESSAGE, null, arc);
            propertyChangeListener.propertyChange(changeEvent);
        }

        for (Annotation annotation : net.getAnnotations()) {
            PropertyChangeEvent changeEvent =
                    new PropertyChangeEvent(net, PetriNet.NEW_ANNOTATION_CHANGE_MESSAGE, null, annotation);
            propertyChangeListener.propertyChange(changeEvent);
        }

        for (RateParameter rateParameter : net.getRateParameters()) {
            PropertyChangeEvent changeEvent =
                    new PropertyChangeEvent(net, PetriNet.NEW_RATE_PARAMETER_CHANGE_MESSAGE, null, rateParameter);
            propertyChangeListener.propertyChange(changeEvent);
        }
    }

    /**
     * Loads and creates a Petri net located at the given file
     * @param file location of the XML file which contains a PNML representation of a Petri net
     * @throws UnparsableException if the file cannot be parsed 
     */
    public void createNewTabFromFile(File file) throws UnparsableException {
        try {
            manager.createFromFile(file);
        } catch (FileNotFoundException | JAXBException e) {
            throw new UnparsableException("Could not initialise Petri net reader!", e);
        }
    }

    /**
     * Save the currently displayed petri net to the specified file
     * @param outFile location to save the Petri net
     * @throws ParserConfigurationException configuration error 
     * @throws TransformerException transformer error 
     * @throws IllegalAccessException illegal access
     * @throws NoSuchMethodException method not found 
     * @throws InvocationTargetException invocation error 
     */
    public void saveAsCurrentPetriNet(File outFile)
            throws ParserConfigurationException, TransformerException, IllegalAccessException, NoSuchMethodException,
            InvocationTargetException {
        PetriNetController petriNetController = getActivePetriNetController();
        PetriNet petriNet = petriNetController.getPetriNet();

        try {
            manager.savePetriNet(petriNet, outFile);
        } catch (JAXBException | IOException e) {
            throw new RuntimeException("Failed to write!", e);
        }
        petriNetController.save();
    }

    /**
     *
     * @return the active Petri net controller
     */
    public PetriNetController getActivePetriNetController() {
        return netControllers.get(activeTab);
    }

    /**
     * @return true if the current petri net has changed
     */
    public boolean hasCurrentPetriNetChanged() {
        PetriNetController activeController = getActivePetriNetController();
        return activeController != null && activeController.hasChanged();
    }

    public boolean anyNetsChanged() {
        return !getNetsChanged().isEmpty();
    }

    /**
     * @return the names of the petri nets that have changed
     */
    public Set<String> getNetsChanged() {
        Set<String> changed = new HashSet<>();
        for (PetriNetController controller : netControllers.values()) {
            if (controller.hasChanged()) {
                changed.add(controller.getPetriNet().getNameValue());
            }
        }
        return changed;
    }

    /**
     * Removes the active tab from display if it exists.
     * Note active tab must be removed from netControllers before the petri net is removed
     * from the manager because the manager will fire a message which causes the active tab
     * to be swapped to the new open tab
     */
    public void removeActiveTab() {
        if (activeTab != null) {
            PetriNetController controller = netControllers.get(activeTab);
            netControllers.remove(activeTab);
            PetriNet petriNet = controller.getPetriNet();
            manager.remove(petriNet);
        }
    }

    /**
     *
     * @return the current active tab
     */
    public PetriNetTab getActiveTab() {
        return activeTab;
    }
}
