package classes;

import com.google.gson.annotations.Expose;

import interfaces.GraphEditable;
import interfaces.LocalMode;
import ui.controller.FlexManager;
import ui.controller.SingletonControl;
import ui.model.Model;
import ui.view.IndexTranslator;

import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Point2D.Double;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.ListIterator;

/**
 * The class "HolonElement" represents any possible element that can be added to
 * a CpsObject (such as TV (consumer) or any energyPerElement source/producer).
 *
 * @author Gruppe14
 */
public class HolonElement implements LocalMode, GraphEditable{

    /** Points of new TestGraph 
     * Represent the Graph 
     * the X component from a Point is period from 0..1
     * the Y component from a Point is the percentage from 0..1
     * */
    private LinkedList<Point2D.Double> graphPoints;
    /** Name of the object, e.g. House, 1 */
    @Expose
    private String objName;
    /** Name of the gadget, e.g. TV */
    @Expose
    private String eleName;
    /** Amount of same elements */
    @Expose
    private int amount;
    /** Currently used energy per element (- indicates consumation of energy, + indicates production of energy)*/
    @Expose
    private float energyPerElement;
    /** Whether the gadget is active or not (currently uses/produces the energy in energyPerElement) */
    @Expose
    private boolean active;
    /** Gives us whether this element is flexible and can flexibly use any part of the energy in flexibleEnergyAvailable */

    
    @Expose
    private Priority priority;

	public enum Priority {
    	Essential, High, Medium , Low
    }

    
    /** Place where the Object is Stored */
    @Expose
    private Pair<String, String> saving;
    /** ID */
    @Expose
    private int id;
    
    
    public java.util.List<Flexibility> flexList = new ArrayList<Flexibility>();
    
    /*
     * Energy at each point of the graph with 100 predefined points. At the
     * beginning, it starts with all values at energyPerElement.
     * If switched to flexible, this represents the maximum of usable energy
     */
    private float[] curveSample;
    
    @Expose
    private int localPeriod;
    @Expose
    private boolean isUsingLocalPeriod;

    /**
     * Create a new HolonElement with a user-defined name, amount of the same
     * element, energyPerElement and corresponding model.
     *
     * @param eleName String
     * @param amount  int
     * @param energy  float
     * @param model Model
     */
    public HolonElement(String eleName, int amount, float energy, Model model) {
    	
    	this(eleName, amount, energy, IdCounterElem.nextId(),model);
    }
    

    /**
     * same as standard constructor, but with already given id (so the counter is not increased twice)
     */
    public HolonElement(String eleName, int amount, float energy, int id, Model model){
    	setUseLocalPeriod(false);
    	setEleName(eleName);
        setAmount(amount);
        setEnergyPerElement(energy);
        setActive(true);
        setGraphPoints(new LinkedList<>());
        initGraphPoints();
        sampleGraph();
        setId(id);
        this.priority = Priority.Low;
    }

    /**
     * Create a copy of the HolonElement given each one a new ID.
     *
     * @param element element to copy
     */
    public HolonElement(HolonElement element) {
    	this.priority = element.getPriority();
    	setLocalPeriod(element.getLocalPeriod());
    	setUseLocalPeriod(element.isUsingLocalPeriod());
        setEleName(element.getEleName());
        setLocalPeriod(element.getLocalPeriod());
        setAmount(element.getAmount());
        setEnergyPerElement(element.getEnergyPerElement());
        setActive(element.isActive());
        setGraphPoints(new LinkedList<>());
        for (Point2D.Double p : element.getGraphPoints()) {
            this.graphPoints.add(new Point2D.Double(p.getX(), p.getY()));
        }
        sampleGraph();
        setSaving(null);
        setId(IdCounterElem.nextId());

    }

	public String getObjName() {
		return objName;
	}

	public void setObjName(String objName) {
		this.objName = objName;
	}



    /**
     * Get the user-defined Name.
     *
     * @return the name String
     */
    public String getEleName() {
        return eleName;
    }

    /**
     * Set the name to any new name.
     *
     * @param name the name to set
     */
    public void setEleName(String name) {
        this.eleName = name;
    }

    /**
     * Get the actual amount of Elements in the selected Object.
     *
     * @return the amount int
     */
    public int getAmount() {
        return amount;
    }

    /**
     * Set the amount of the Element in the selected Object.
     *
     * @param amount the amount to set
     */
    public void setAmount(int amount) {
        this.amount = amount;
    }

    /**
     * Get the energyPerElement value of the selected Element.
     *
     * @return the energyPerElement
     */
    public float getEnergyPerElement() {
        return energyPerElement;
    }
    public Priority getPriority() {
    	return priority;
    }
    
    
    public void setPriority(Priority priority) {
    	this.priority = priority;
    }    
    /**
     *  Check the HolonElemnet is a Producer
     * 	@return true when the energy used be each element is higher then 0
     */
    public boolean isProducer()
    {
    	return (energyPerElement > 0);
    }
    
    /**
     *  Check the HolonElemnet is a Consumer
     * 	@return true when the energy used be each element is lower then 0
     */
    public boolean isConsumer()
    {
    	return (energyPerElement < 0);
    }

    /**
     * Set the energyPerElement value of the selected Element.
     *
     * @param energyPerElement the energyPerElement to set
     */
    public void setEnergyPerElement(float energyPerElement) {
        this.energyPerElement = energyPerElement;
    }

    /**
     * Get the Status of the Element (see description of variables).
     *
     * @return the active
     */
    public boolean isActive() {
        return active;
    }

    public boolean isOn(FlexManager flexManager) {
    	//return (flexManager.isAFlexInUseOfHolonElement(this))?!active:active; 
    	//Bool logic XOR
    	return flexManager.isAFlexInUseOfHolonElement(this) ^ active;
    }
    public boolean isFlexActive(FlexManager flexManager) {
    	return flexManager.isAFlexInUseOfHolonElement(this);
    }
    
    /**
     * Set the Status of the Element (see description of variables).
     *
     * @param active the active to set
     */
    public void setActive(boolean active) {
        this.active = active;
    }

    /**
     * Multiply the amount of gadgets, given by the user, and the
     * consumption/production. If the switch isWorking is turned off for on
     * gadget, the energyPerElement of this gadget have to be subtracted.
     *
     * @return totalEnergy (actual)
     */
    public float getMaximumEnergy() {
        return amount * energyPerElement;
    }

    /**
     * Get the energyPerElement value at a selected time x.
     *
     * @param timestep int
     * @return energyPerElement value
     */
    public float getOverallEnergyAtTimeStep(int timestep) {
       return ((float) amount) * energyPerElement * curveSample[IndexTranslator.getEffectiveIndex(this, timestep)];
    }
    /**
     * Get the energyPerElement currently(at given time step) available
     */
    public float getEnergyAtTimeStep(int timestep) {
    	return amount * energyPerElement * this.curveSample[IndexTranslator.getEffectiveIndex(this, timestep)];
    }


    /**
     * Get the Id of the selected HolonElement.
     *
     * @return id the id
     */
    public int getId() {
        return id;
    }

    /**
     * Set the ID of the HolonElement (one time only).
     *
     * @param id the id
     */
    public void setId(int id) {
        this.id = id;
    }

    /**
     * @return the saving
     */
    public Pair<String, String> getSaving() {
        return saving;
    }

    /**
     * @param saving the saving to set
     */
    public void setSaving(Pair<String, String> saving) {
        this.saving = saving;
    }


    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("[HolonElement: ");
        sb.append("id=").append(id)
                .append(", eleName=").append(eleName)
                .append(", amount=").append(amount)
                .append(", active=").append(active)
                .append(", energyPerElement used=").append(energyPerElement);
        sb.append("]");

        return sb.toString();
    }

    /**
     * Initialize the {@link HolonElement#graphPoints} List with the normal 2 Points at 100%.
     */
	private void initGraphPoints()
	{
		graphPoints.clear();
		graphPoints.add(new Point2D.Double(0,1.0));
		graphPoints.add(new Point2D.Double(1,1.0));
	}
	/**
	 * Getter for the graphPoint List.
	 * @return {@link HolonElement#graphPoints}
	 */
	public LinkedList<Point2D.Double> getGraphPoints() {
		return graphPoints;
	}

	/**
	 * Setter for the graphPoint List.
	 * @param testGraphPoints is new  {@link HolonElement#graphPoints}
	 */
	public void setGraphPoints(LinkedList<Point2D.Double> testGraphPoints) {
		this.graphPoints = testGraphPoints;
	}

	
	
	
	//interfaces.GraphEditable
	@Override
	public Graphtype getGraphType() {
		return Graphtype.doubleGraph;
	}


	@Override
	public LinkedList<Double> getStateGraph() {
		return getGraphPoints();
	}

	
	@Override
	public void sampleGraph() {
		curveSample = sampleGraph(100);
	}

	@Override
	public void reset() {
		initGraphPoints();
		sampleGraph();
	}
	/**
	 * Generate out of the Graph Points a array of floats that represent the Curve at each sample position. The Values are in the Range [0,1]. 
	 * e.g. 0.0 represent: "0%" , 0.34 represent: 34%  1.0 represent: "100%" 
	 * @param sampleLength amount of samplePositions. The positions are equidistant on the Range[0,1].
	 * @return the float array of samplepoints.
	 */
	private float[] sampleGraph(int sampleLength)
	{		
		ListIterator<Point2D.Double> iter = this.graphPoints.listIterator();
		Point.Double before = iter.next();
		Point.Double after = iter.next();
		float [] sampleCurve = new float[sampleLength];	
		for(int i = 0; i<sampleLength ; i++)
		{
			double graphX = (double)i / (double) (sampleLength - 1); //from 0.0 to 1.0
			if(graphX > after.x)
			{
				before = after;
				after = iter.next();
			}
			//t to determine how many percentage the graphX is to the next Point needed to calc Bezier
			//inverseLerp(valueBetween, min, max) (valueBetween - min) / (max - min)
			// e.g. old.x = 0.4, actual.x = 0.8 and graphX = 0.6 then t is 0.5
			double t = (after.x -before.x > 0)? (graphX - before.x) / (after.x -before.x) : 0.0;			
			sampleCurve[i] = (float) getYBetweenTwoPoints(t, before, after);
		}
		return sampleCurve;
	}
	
	/**
	 * Helper method for {@link HolonElement#sampleGraph(int)}.
	 * <p>
	 * Its get the start and Endposition and calculate the Points in between for the Bezi�r Curve.
	 * Then its get the Y Value a.k.a. the percentage from the curve at the X value t.  
	 * @param t is in Range [0,1] and represent how much the X value is traverse along the Curve between the two Points.
	 * @param start is the start Point of the Curve.
	 * @param end is the end Point of the Curve.
	 * @return the percentage from the Curve at the X Value based on t.
	 */
	private double getYBetweenTwoPoints(double t, Point.Double start, Point.Double end) {
		
		double mitte = (start.x + end.x)* 0.5;
		Point.Double bezier = getBezierPoint(t, start, new Point.Double(mitte, start.y), new Point.Double(mitte, end.y), end);
		return bezier.y;
	}
	/**
	 * Helper method for {@link HolonElement#getYBetweenTwoPoints(double, Point.Double, Point.Double)}.
	 * <p>
	 * A Method for a normal Cubic Bezier Curve. A Cubic Bezier curve has four control points.
	 * @param t is in Range [0,1] how much it traverse along the curve.
	 * @param p0 StartPoint
	 * @param p1 ControlPoint
	 * @param p2 ControlPoint
	 * @param p3 EndPoint
	 * @return the BezierPosition at t.
	 */
	private Point.Double getBezierPoint(double t, Point.Double p0, Point.Double p1,Point.Double p2,Point.Double p3) {
		/*
		 * Calculate Bezi�r:
		 * B(t) = (1-t)^3 * P0 + 3*(1-t)^2 * t * P1 + 3*(1-t)*t^2 * P2 + t^3 * P3 , 0 < t < 1
		 * 
		 * Source: //http://www.theappguruz.com/blog/bezier-curve-in-games
		 */
		Point.Double bezier = new Point.Double();
		double OneSubT =  1-t;
		double OneSubT2 = Math.pow(OneSubT, 2);
		double OneSubT3 = Math.pow(OneSubT, 3);
		double t2 = Math.pow(t , 2);
		double t3 = Math.pow(t , 3);
		
		bezier.x = OneSubT3 * p0.x + 3 * OneSubT2 * t * p1.x + 3 * OneSubT * t2 * p2.x + t3 * p3.x;
		bezier.y = OneSubT3 * p0.y + 3 * OneSubT2 * t * p1.y + 3 * OneSubT * t2 * p2.y + t3 * p3.y;
		return bezier;
		
	}
	//interfaces.LocalMode
	@Override
	public void setLocalPeriod(int period) {
		localPeriod=period;
	}
	
	@Override
	public int getLocalPeriod() {
		return localPeriod;
	}
	
	@Override
	public boolean isUsingLocalPeriod() {
		return isUsingLocalPeriod;
	}
	
	@Override
	public void setUseLocalPeriod(boolean state) {
		this.isUsingLocalPeriod=state;
	}
	
}
