package ui.controller;

import classes.*;
import classes.comparator.EnergyMinToMaxComparator;
import classes.comparator.MinEnergyComparator;
import classes.comparator.TotalEnergyComparator;
import ui.model.Model;
import ui.view.FlexiblePane;
import ui.view.MyCanvas;

import java.util.ArrayList;
import java.util.HashMap;

/**
 * Controller for Simulation.
 *
 * @author Gruppe14
 */
public class SimulationManager {
    int global = 0;
    private Model model;
    private ArrayList<AbstractCpsObject> objectsToHandle;
    //	private ArrayList<CpsEdge> allConnections;
    private ArrayList<SubNet> subNets;
    private ArrayList<CpsEdge> brokenEdges;
    private MyCanvas canvas;
    private int timeStep;
    private HashMap<Integer, Float> tagTable = new HashMap<>();
    private FlexiblePane flexPane;

    private HashMap<HolonElement, Float> flexDevicesTurnedOnThisTurn = new HashMap<>();

    /**
     * Constructor.
     *
     * @param m Model
     */
    SimulationManager(Model m) {
        canvas = null;
        model = m;
        subNets = new ArrayList<>();
        brokenEdges = new ArrayList<>();
    }

    /**
     * calculates the flow of the edges and the supply for objects.
     *
     * @param x current Iteration
     */
    void calculateStateForTimeStep(int x) {
        reset();
        timeStep = x;
        searchForSubNets();
        for (SubNet singleSubNet : subNets) {
            resetConnections(singleSubNet.getObjects().get(0), new ArrayList<>(), new ArrayList<>());
        }
        for (SubNet singleSubNet : subNets) {
            float production = calculateEnergyWithoutFlexDevices("prod", singleSubNet, timeStep);
            float consumption = calculateEnergyWithoutFlexDevices("cons", singleSubNet, timeStep);
            // surplus of energy is computed by sum, since consumption is a negative value
            float energySurplus = production + consumption;

            float minConsumption = calculateMinimumEnergy(singleSubNet, timeStep);

            // --------------- use flexible devices ---------------
            if (energySurplus != 0 && model.useFlexibleDevices()) {
                turnOnFlexibleDevices(singleSubNet, energySurplus, x);

//                if (!flexDevicesTurnedOnThisTurn.isEmpty()) {
//                    System.out.println("The following devices were turned on in this turn: ");
//                    System.out.println(flexDevicesTurnedOnThisTurn.toString());
//                }

                // recompute after having examined/turned on all flexible devices
                production = calculateEnergyWithFlexDevices("prod", singleSubNet, timeStep);
                consumption = calculateEnergyWithFlexDevices("cons", singleSubNet, timeStep);
                energySurplus = production + consumption;
            }

            // --------------- set flow simulation ---------------
            setFlowSimulation(singleSubNet);

            // --------------- visualise graph ---------------
           
            
            /**
             * production of subnets, that might be partially turned on/off
             */
            float currentProduction = production;
            /**
             * HolonObjects that can be partially Supplied but might be fully Supplied
             */
            ArrayList<HolonObject> partiallySuppliedList = new ArrayList<HolonObject>();
            /**
             * supply Buildings with minimal Energy first, if conflicts happen
             */
            singleSubNet.getObjects().sort(new MinEnergyComparator(x));
            for (HolonObject hl : singleSubNet.getObjects()) {
                if (hl.getState() != HolonObject.NO_ENERGY
                        && hl.getState() != HolonObject.PRODUCER) {
                    for (int i = 0; i < hl.getConnections().size(); i++) {
                        CpsEdge edge = hl.getConnectedTo().get(i);
                        if (edge.isWorking() && edge.getFlow() > 0
                                || edge.getCapacity() == CpsEdge.CAPACITY_INFINITE) {
                            if ((production + consumption) >= 0) {
                                if (energySurplus > 0) {
                                    hl.setState(HolonObject.OVER_SUPPLIED);
                                } else {
                                    hl.setState(HolonObject.SUPPLIED);
                                }
                            } else {
                                if ((production + minConsumption) >= 0) {
                                    hl.setState(HolonObject.PARTIALLY_SUPPLIED);
                                    currentProduction += hl.getMinEnergy(x);
                            		partiallySuppliedList.add(hl);
                                } else if (hl.checkIfPartiallySupplied(timeStep)) {
                                    hl.setState(HolonObject.PARTIALLY_SUPPLIED);
                                    currentProduction += hl.getMinEnergy(x);
                            		partiallySuppliedList.add(hl);
                                } else {
                                	/**
                                	 * Case that only some HolonObjects can be supplied
                                	 */
                                	if(-hl.getCurrentEnergyAtTimeStep(x)<=currentProduction){
                                		hl.setState(HolonObject.PARTIALLY_SUPPLIED);
                                		currentProduction += hl.getMinEnergy(x);
                                		partiallySuppliedList.add(hl);
                                	}else if(-hl.getMinEnergy(x)<=currentProduction){
                                		hl.setState(HolonObject.PARTIALLY_SUPPLIED);
                                		currentProduction += hl.getMinEnergy(x);
                                		partiallySuppliedList.add(hl);
                                	}else{
                                		hl.setState(HolonObject.NOT_SUPPLIED);
                                		//currentProduction += hl.getCurrentEnergyAtTimeStep(x);
                                		
                                	}
                                }
                            }
                            break;
                        }
                    }
                    
                    
                    /**
                     * check if some object cn self supply itself
                     */
                    if (hl.checkIfPartiallySupplied(timeStep)
                            && hl.getState() != HolonObject.SUPPLIED
                            && hl.getState() != HolonObject.OVER_SUPPLIED) {
                        hl.setState(HolonObject.PARTIALLY_SUPPLIED);
                    }
                }
            }
            
            /**
             * check if some partially supplied building might be fully supplied.
             */
            partiallySuppliedList.sort(new EnergyMinToMaxComparator(x));
            for(HolonObject part: partiallySuppliedList){
            	currentProduction -= part.getMinEnergy(x);
            	/*
            	 * if possible, supply fully
            	 */
            	if(-part.getCurrentEnergyAtTimeStep(x)<=currentProduction){
            		part.setState(HolonObject.SUPPLIED);
            		currentProduction += part.getCurrentEnergyAtTimeStep(x);
            	}else{
            		currentProduction += part.getMinEnergy(x);
            	}
            }
        }
        canvas.repaint();
        flexPane.recalculate();
    }

    /**
     * search for all flexible devices in the network and turn them on, until energy surplus = 0
     * or all devices have been examined.
     *
     * This code could be compressed (cases inside over- and underproduction are the same), but we decided that it
     * is better readable this way
     *
     * @param subNet        the subnet
     * @param energySurplus the current surplus of energy
     */
    private void turnOnFlexibleDevices(SubNet subNet, float energySurplus, int timestep) {
        for (HolonObject holonOb : subNet.getObjects()) {
            for (HolonElement holonEl : holonOb.getElements()) {

                // if this element is flexible and active (can be considered for calculations)
                if (holonEl.isFlexible() && holonEl.isActive()) {
                    float energyAvailableSingle = holonEl.getAvailableEnergyAt(timestep);
                    float energyAvailableMultiple = energyAvailableSingle * holonEl.getAmount();

                    // ------------- flexible consumer / OVERPRODUCTION -------------
                    if (energyAvailableMultiple < 0 && energySurplus > 0) {

                        // if there is more wasted energy than energy that this device can give, give all energy available
                        if (Math.abs(energyAvailableMultiple) <= Math.abs(energySurplus)) {
                            energySurplus += energyAvailableMultiple;
                            // set the new energy consumption to the maximum
                            holonEl.setEnergyPerElement(energyAvailableSingle);
                            flexDevicesTurnedOnThisTurn.put(holonEl, energyAvailableMultiple);
                        }
                        // else: we just need to turn on part of the flexible energy available
                        else {
                            float energyNeeded = -energySurplus;
                            energySurplus += energyNeeded;   // should give 0, but was kept this was for consistency
                            // the energy needed divided through the amount of elements
                            holonEl.setEnergyPerElement(energyNeeded / holonEl.getAmount());
                            flexDevicesTurnedOnThisTurn.put(holonEl, energyNeeded);
                        }
                    }

                    // ------------- flexible producer / UNDERPRODUCTION -------------
                    else if (energyAvailableMultiple > 0 && energySurplus < 0) {

                        // if there is more energy needed than this device can give, give all the energy available
                        if (Math.abs(energyAvailableMultiple) <= Math.abs(energySurplus)) {
                            energySurplus += energyAvailableMultiple;
                            // set the new energy consumption to the maximum
                            holonEl.setEnergyPerElement(energyAvailableSingle);
                            flexDevicesTurnedOnThisTurn.put(holonEl, energyAvailableMultiple);
                        }
                        // else: we just need to turn on part of the flexible energy available
                        else {
                            float energyNeeded = -energySurplus;
                            int i = 0;
                            energySurplus += energyNeeded;   // should give 0, but was kept this was for consistency
                            // the energy needed divided through the amount of elements
                            holonEl.setEnergyPerElement(energyNeeded / holonEl.getAmount());
                            flexDevicesTurnedOnThisTurn.put(holonEl, energyNeeded);
                        }

                    }
                }

                if (energySurplus == 0) {
                    break;
                }
            }

            if (energySurplus == 0) {
                break;
            }
        }
    }

    /**
     * Set Flow Simulation.
     *
     * @param sN Subnet
     */
    private void setFlowSimulation(SubNet sN) {
        ArrayList<AbstractCpsObject> producers = new ArrayList<>();
        AbstractCpsObject tmp = null;
        tagTable = new HashMap<>();
        // traverse all objects in this subnet
        for (HolonObject hl : sN.getObjects()) {
            float energy = hl.getCurrentEnergyAtTimeStep(timeStep);
            // if their production is higher than their consumption
            if (energy > 0) {
                tagTable.put(hl.getId(), energy);
                hl.addTag(hl.getId());
                for (CpsEdge edge : hl.getConnections()) {
                    if (edge.isWorking()) {
                        // set other end of edge as tmp-object
                        // and add this end to the other end's tag-list
                        AbstractCpsObject a = edge.getA();
                        AbstractCpsObject b = edge.getB();
                        if (a.getId() == hl.getId()) {
                            b.addTag(hl.getId());
                            tmp = b;
                        }
                        if (b.getId() == hl.getId()) {
                            a.addTag(hl.getId());
                            tmp = a;
                        }

                        edge.setFlow(edge.getFlow() + energy);
                        edge.calculateState();
                        edge.addTag(hl.getId());
                        if (edge.isWorking() && !producers.contains(tmp)) {
                            if (tmp instanceof HolonSwitch) {
                                if (((HolonSwitch) tmp).getState(timeStep)) {
                                    producers.add(tmp);
                                }
                            } else if (!(tmp instanceof CpsUpperNode)) {
                                producers.add(tmp);
                            }
                        }
                    }
                }
            }
        }
        setFlowSimRec(producers, 0);
    }

    /**
     * Set Flow Simulation Rec.
     *
     * @param nodes the nodes
     * @param iter  the Iteration
     */
    private void setFlowSimRec(ArrayList<AbstractCpsObject> nodes, int iter) {
        ArrayList<AbstractCpsObject> newNodes = new ArrayList<>();
        ArrayList<CpsEdge> changedEdges = new ArrayList<>();
        AbstractCpsObject tmp;
        if (nodes.size() != 0) {
            for (AbstractCpsObject cps : nodes) {
                // check whether the cps is in a legit state if it is a switch
                if (legitState(cps)) {
                    for (CpsEdge edge : cps.getConnections()) {
                        // is edge working
                        // and does the edge's tag-list not (yet) contain all tags of the cps
                        if (edge.isWorking()
                                && (!(edge.containsTags(edge.getTags(), cps.getTag())))) {
                            if (edge.getA().getId() == cps.getId()) {
                                tmp = edge.getB();
                            } else {
                                tmp = edge.getA();
                            }
                            for (Integer tag : cps.getTag()) {
                                if (!(edge.getTags().contains(tag))
                                        && !(edge.getPseudoTags().contains(tag))) {
                                    edge.setFlow(edge.getFlow() + tagTable.get(tag));
                                    edge.addTag(tag);
                                }
                            }
                            // uppernodes do not spread energy
                            if (!(tmp instanceof CpsUpperNode)) {
                                for (Integer tag : tmp.getTag()) {
                                    if (!(edge.getTags().contains(tag))
                                            && tagTable.get(tag) != null
                                            && !(edge.getPseudoTags().contains(tag))) {
                                        edge.setFlow(edge.getFlow() + tagTable.get(tag));
                                        edge.addPseudoTag(tag);
                                        changedEdges.add(edge);
                                    }
                                }
                            }
                            edge.calculateState();
                            if (edge.isWorking()
                                    && !(tmp instanceof CpsUpperNode)) {
                                tmp.addAllPseudoTags(cps.getTag());
                                if (!newNodes.contains(tmp)) {
                                    newNodes.add(tmp);
                                }
                            }
                        }
                    }
                }
            }
            setPseudoTags(newNodes, changedEdges);
            setFlowSimRec(newNodes, iter + 1);
        }
    }

    /**
     * Set the Pseudo Tags.
     *
     * @param nodes Array of AbstractCpsObjects
     */
    private void setPseudoTags(ArrayList<AbstractCpsObject> nodes, ArrayList<CpsEdge> edges) {
        for (AbstractCpsObject node : nodes) {
            node.recalculateTags();
            node.setPseudoTags(new ArrayList<>());
        }
        for (CpsEdge edge : edges) {
            edge.recalculateTags();
            edge.setPseudoTag(new ArrayList<>());
        }
    }

    /**
     * Reset the Connection.
     *
     * @param cps          CpsObject
     * @param visitedObj   the visited Objects
     * @param visitedEdges the visited Edges
     */
    private void resetConnections(AbstractCpsObject cps, ArrayList<Integer> visitedObj,
                                  ArrayList<CpsEdge> visitedEdges) {
        visitedObj.add(cps.getId());
        cps.resetTags();
        for (CpsEdge e : cps.getConnections()) {
            if (!(visitedEdges.contains(e))) {
                e.setFlow(0);
                e.calculateState();
                e.setTags(new ArrayList<>());
                visitedEdges.add(e);
                if (!(visitedObj.contains(e.getA().getId()))) {
                    resetConnections(e.getA(), visitedObj, visitedEdges);
                    e.getA().resetTags();
                }
                if (!(visitedObj.contains(e.getB().getId()))) {
                    resetConnections(e.getB(), visitedObj, visitedEdges);
                    e.getB().resetTags();
                }
            }
        }
    }

    /**
     * calculates the energy of either all producers or consumers.
     * Flexible devices are filtered out
     *
     * @param type Type
     * @param sN   Subnet
     * @param x    Integer
     * @return The Energy
     */
    private float calculateEnergyWithoutFlexDevices(String type, SubNet sN, int x) {
        float energy = 0;
        for (HolonObject hl : sN.getObjects()) {
            float currentEnergyWithoutFlexibles = hl.getCurrentEnergyAtTimeStepWithoutFlexiblesAndResetFlexibles(x);

            if (type.equals("prod")) {
                if (currentEnergyWithoutFlexibles > 0) {
                    energy += currentEnergyWithoutFlexibles;
                    hl.setState(HolonObject.PRODUCER);
                }
            }
            if (type.equals("cons")) {
                if (currentEnergyWithoutFlexibles < 0) {
                    energy = energy + currentEnergyWithoutFlexibles;
                    hl.setState(HolonObject.NOT_SUPPLIED);
                }
            }
            if (currentEnergyWithoutFlexibles == 0) {
                hl.setState(HolonObject.NO_ENERGY);
            }
        }
        return energy;
    }

    /**
     * calculates the energy of either all producers or consumers.
     * Flexible devices are filtered out
     *
     * @param type Type
     * @param sN   Subnet
     * @param x    Integer
     * @return The Energy
     */
    private float calculateEnergyWithFlexDevices(String type, SubNet sN, int x) {
        float energy = 0;
        for (HolonObject hl : sN.getObjects()) {
            float currentEnergy = hl.getCurrentEnergyAtTimeStep(x);

            if (type.equals("prod")) {
                if (currentEnergy > 0) {
                    energy += currentEnergy;
                    hl.setState(HolonObject.PRODUCER);
                }
            }
            if (type.equals("cons")) {
                if (currentEnergy < 0) {
                    energy = energy + currentEnergy;
                    hl.setState(HolonObject.NOT_SUPPLIED);
                }
            }
            if (currentEnergy == 0) {
                hl.setState(HolonObject.NO_ENERGY);
            }
        }
        return energy;
    }

    /**
     * Calculate the Minimum Energy.
     *
     * @param sN Subnet
     * @param x  Integer
     * @return the Calculated minimum Energy
     */
    private float calculateMinimumEnergy(SubNet sN, int x) {
        float min = 0;
        float minElement = 0;
        for (HolonObject hl : sN.getObjects()) {
            if (hl.getElements().size() > 0 && hl.getElements().get(0).getOverallEnergyAtTimeStep(x) < 0) {
                minElement = hl.getElements().get(0).getOverallEnergyAtTimeStep(x);
            }
            for (HolonElement he : hl.getElements()) {
                float overallEnergy = he.getOverallEnergyAtTimeStep(x);
                if (minElement < overallEnergy && overallEnergy < 0) {
                    minElement = overallEnergy;
                }
            }
            min = min + minElement;
        }
        return min;
    }

    /**
     * generates all subNets from all objectsToHandle.
     */
    private void searchForSubNets() {
        subNets = new ArrayList<>();
        brokenEdges.clear();
        boolean end = false;
        int i = 0;
        AbstractCpsObject cps;
        if (objectsToHandle.size() > 0) {
            while (!end) {
                cps = objectsToHandle.get(i);
                SubNet singleSubNet = new SubNet(new ArrayList<>(), new ArrayList<>(),
                        new ArrayList<>());
                singleSubNet = buildSubNet(cps, new ArrayList<>(), singleSubNet);
                if (singleSubNet.getObjects().size() != 0) {
                    subNets.add(singleSubNet);
                }
                if (0 == objectsToHandle.size()) {
                    end = true;
                }
            }
        }
    }

    /**
     * recursivly generates a subnet of all objects, that one specific object is
     * connected to.
     *
     * @param cps     AbstractCpsObject
     * @param visited visited Array of Integer
     * @param sN      Subnets
     * @return Subnet
     */
    private SubNet buildSubNet(AbstractCpsObject cps, ArrayList<Integer> visited, SubNet sN) {
        visited.add(cps.getId());
        if (cps instanceof HolonObject) {
            sN.getObjects().add((HolonObject) cps);
        }
        if (cps instanceof HolonSwitch) {
            sN.getSwitches().add((HolonSwitch) cps);
        }
        removeFromToHandle(cps.getId());
        AbstractCpsObject a;
        AbstractCpsObject b;
        for (CpsEdge edge : cps.getConnections()) {
            if (edge.isWorking()) {
                a = edge.getA();
                b = edge.getB();
                if (!(cps instanceof HolonSwitch)) {
                    if (!(sN.getEdges().contains(edge))) {
                        sN.getEdges().add(edge);
                    }
                }
                if (cps instanceof HolonSwitch && ((HolonSwitch) cps).getState(timeStep)) {
                    if (!(sN.getEdges().contains(edge))) {
                        sN.getEdges().add(edge);
                    }
                }
                if (!visited.contains(a.getId()) && legitState(cps) && !(a instanceof CpsUpperNode)) {
                    sN = buildSubNet(a, visited, sN);
                }
                if (!visited.contains(b.getId()) && legitState(cps) && !(b instanceof CpsUpperNode)) {
                    sN = buildSubNet(b, visited, sN);
                }
                if (a instanceof CpsUpperNode && a.getId() != cps.getId()) {
                    edge.setConnected(CpsEdge.CON_UPPER_NODE_NOT_INSIDE);
                    checkForConnectedStates(b, (CpsUpperNode) a, edge);
                }
                if (b instanceof CpsUpperNode && b.getId() != cps.getId()) {
                    edge.setConnected(CpsEdge.CON_UPPER_NODE_NOT_INSIDE);
                    checkForConnectedStates(a, (CpsUpperNode) b, edge);
                }
            } else {
                brokenEdges.add(edge);
            }
        }
        return sN;
    }

    /**
     * is the Switch in a legitimate State.
     *
     * @param current AbstractCpsObject
     * @return boolean
     */
    private boolean legitState(AbstractCpsObject current) {
        return !(current instanceof HolonSwitch)
                || ((HolonSwitch) current).getState(timeStep);
    }

//    /**
//     * ensures that objectsToHandle only contains HolonObjects.
//     */
//	public void cleanObjectsToHandle() {
//		for (int i = 0; i < objectsToHandle.size(); i++) {
//			if (!(objectsToHandle.get(i) instanceof HolonObject)) {
//				objectsToHandle.remove(i);
//			}
//		}
//	}

    /**
     * removes an Object that already has been handled.
     *
     * @param id the Object ID
     */
    private void removeFromToHandle(int id) {
        for (int i = 0; i < objectsToHandle.size(); i++) {
            if (objectsToHandle.get(i).getId() == id) {
                objectsToHandle.remove(i);
            }
        }
    }

    /**
     * copies the data of an array of Objects.
     *
     * @param toCopy the ArrayList of CpsObjects co Copy
     */
    private void copyObjects(ArrayList<AbstractCpsObject> toCopy) {
        for (AbstractCpsObject cps : toCopy) {
            if (cps instanceof CpsUpperNode) {
                copyObjects(((CpsUpperNode) cps).getNodes());
            } else {
                objectsToHandle.add(cps);
            }
        }
    }

    /**
     * Prints the Components auf all subnets.
     */
    private void printNetsToConsole() {
        for (int i = 0; i < subNets.size(); i++) {
            SubNet subNet = subNets.get(i);
            System.out.println("SUBNET NR:" + i);
            subNet.toString(timeStep);
        }
    }

    /**
     * Set the Canvas.
     *
     * @param can the Canvas
     */
    public void setCanvas(MyCanvas can) {
        canvas = can;
    }

    /**
     * Reset all Data to the current state of the Model.
     */
    public void reset() {
        objectsToHandle = new ArrayList<>();
        copyObjects(model.getObjectsOnCanvas());
        flexDevicesTurnedOnThisTurn = new HashMap<>();
    }

    /**
     * Resets the State of all Edges
     */
    private void resetEdges() {
        for (CpsEdge e : brokenEdges) {
            e.setWorkingState(true);
        }
    }

    /**
     * Resets the State for the whole Simulation Model
     */
    void resetSimulation() {
        reset();
        resetEdges();
    }

    /**
     * Get all Subnets.
     *
     * @return all Subnets
     */
    public ArrayList<SubNet> getSubNets() {
        return subNets;
    }

    /**
     * Get broken Edges
     */
//    public ArrayList<CpsEdge> getBrokenEdges() {
//        return brokenEdges;
//    }

    /**
     * checks whether a given object is connected to an object inside the upperNode.
     * if yes, the state for the edge is changed in "connected" or "not connected"
     */
    private void checkForConnectedStates(AbstractCpsObject cps, CpsUpperNode cUNode, CpsEdge theEdge) {
        AbstractCpsObject tmp;
        for (CpsEdge edge : cps.getConnections()) {
            if (edge.getA().getId() == cps.getId()) {
                tmp = edge.getB();
            } else {
                tmp = edge.getA();
            }
            if (cUNode.getNodes().contains(tmp)) {
                if (tmp instanceof CpsUpperNode) {
                    checkForConnectedStates(cps, (CpsUpperNode) tmp, theEdge);
                } else {
                    theEdge.setConnected(CpsEdge.CON_UPPER_NODE_AND_INSIDE);
                    break;
                }
            }
        }
    }

    public FlexiblePane getFlexiblePane() {
        return flexPane;
    }

    void setFlexiblePane(FlexiblePane fp) {
        flexPane = fp;
    }

}
