package interpolation;

import java.awt.*;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.geom.GeneralPath;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;

import javax.swing.*;
import javax.swing.event.ChangeListener;

import gui.TransferableUtils;

public class FloatInterpolator extends GraphInterpolator {

	private static final long serialVersionUID = -6215153672610067743L;
	
	protected ArrayList<Keyframe<Float>> keyframes;
	protected Keyframe<Float> dragFrame;
	
	private Algebra function;
	private JTextField equationField;
	
	private JSpinner manualSpinner;
	
	private JSpinner perciseTime;
	private JSpinner perciseValue;
	private JButton perciseButton;

	public FloatInterpolator(float startingVal, GetSet gs, ChangeListener... listeners) {
		super(gs, listeners);
		
		keyframes = new ArrayList<Keyframe<Float>>();
		keyframes.add(new Keyframe<Float>(0f, startingVal));
		keyframes.add(new Keyframe<Float>(1f, startingVal));
		
		initializeComponents();
		addActionListeners();
		
		gbc.gridwidth=1;
		gbc.weightx = 0;
		add(new JLabel("f(x) ="), gbc);
		gbc.weightx=1;
		gbc.gridx++;
		add(equationField, gbc);
		gbc.gridy++;
		
		JPanel addPanel = new JPanel(new GridLayout(1, 4));
		addPanel.add(new JLabel("Add keyframe:"));
		addPanel.add(perciseTime);
		addPanel.add(perciseValue);
		addPanel.add(perciseButton);
		
		add(addPanel, gbc);
		
		pack();
	}

	private void initializeComponents() {
		
		perciseTime = new JSpinner(new SpinnerNumberModel(0d, 0d, 1d, .05d));
		perciseValue = new JSpinner(new SpinnerNumberModel(0d, 0d, 1d, .05d));
		perciseButton = new JButton("Add");
		
		function = new Algebra(keyframes.get(0).getValue().doubleValue()+"");
		equationField = new JTextField(function.getFunction());
		manualSpinner = new JSpinner(new SpinnerNumberModel(keyframes.get(0).getValue().doubleValue(), 0d, 1d, .01d));
		createMenuBar();
	}
	
	private void addActionListeners() {
		
		manualSpinner.addChangeListener(ce -> {
			fireChangeEvent();
		});
		
		perciseButton.addActionListener(ae -> {
			Keyframe<Float> key = new Keyframe<Float>(((Double) perciseTime.getValue()).floatValue(), ((Double) perciseValue.getValue()).floatValue());
			
			keyframes.add(key);
			
			for(int i = 0; i < keyframes.size(); i++)
				if(Math.abs(keyframes.get(i).getTime()-key.getTime()) < .05f) {
					keyframes.remove(i);
					i--;
				}
			keyframes.add(key);
			Collections.sort(keyframes);
			repaint();
			fireChangeEvent();
		});
		
		equationField.addActionListener(ae -> {
			if(function.setFunction(equationField.getText())) {
				clear(0, 1);
				equationField.setBackground(new Color(255, 255, 255, 255));
				for(float f = 0; f <= 1; f+=.02f)
					keyframes.add(new Keyframe<Float>(f, Math.min(1f, Math.max(0f, function.evalF(f)))));
				keyframes.add(new Keyframe<Float>(1f, Math.min(1f, Math.max(0f, function.evalF(1f)))));
				
				for (int i = 1; i < keyframes.size()-1; i++) {
					if(keyframes.get(i-1).getValue().equals(keyframes.get(i).getValue()) && keyframes.get(i).getValue().equals(keyframes.get(i+1).getValue())) {
						keyframes.remove(i);
						i--;
					}	
				}
				repaint();
				fireChangeEvent();
				animationButton.repaint();
			}
			else {
				equationField.setBackground(new Color(255, 127, 127));
			}
		});
	}
	
	@SuppressWarnings("unchecked")
	private void createMenuBar() {
		JMenuItem copy = new JMenuItem("Copy");
		copy.addActionListener(ae -> TransferableUtils.copyObject(Keyframe.deepCopy(keyframes)));
		copy.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.META_MASK));
		
		JMenuItem paste = new JMenuItem("Paste");
		paste.addActionListener(ae -> {
			Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(this);
			try {
				ArrayList<Keyframe<Serializable>> data = (ArrayList<Keyframe<Serializable>>) t.getTransferData(TransferableUtils.objectDataFlavor);
				if(data.isEmpty())
					return;
				//Yes this is ugly. There is really no better way of doing it though. Forgive me
				if(data.get(0).getValue() instanceof Float)
					keyframes = (ArrayList<Keyframe<Float>>) (Object) data;
				if(data.get(0).getValue() instanceof Double)
					keyframes = Keyframe.doubleToFloat((ArrayList<Keyframe<Double>>) (Object) data);
				if(data.get(0).getValue() instanceof Integer)
					keyframes = Keyframe.intToFloat((ArrayList<Keyframe<Integer>>) (Object) data);
				animationButton.repaint();
				repaint();
				fireChangeEvent();
			} catch (UnsupportedFlavorException | IOException e1) {
				System.err.println("Invalid data copied");
			}
		});
		paste.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.META_MASK));
		
		JMenuBar bar = new JMenuBar();
		JMenu edit = new JMenu("Edit");
		edit.add(copy);
		edit.add(paste);
		bar.add(edit);
		setJMenuBar(bar);
	}
	
	@Override
	protected void graphDragged(float x, float y, boolean rightClick) {
		if(dragFrame != null)
			clear(dragFrame.getTime(), x);
		dragFrame = new Keyframe<Float>(x, y);
		
		repaint();
		fireChangeEvent();
	}
	
	@Override
	protected void graphReleased(float x, float y, boolean rightClick) {
		if(dragFrame == null)
			dragFrame = new Keyframe<Float>(x, y);
		
		keyframes.add(dragFrame);
		
		Collections.sort(keyframes);
		
		if(keyframes.get(keyframes.size()-1).getTime() != 1f)
			keyframes.add(new Keyframe<Float>(1f, dragFrame.getValue()));
		if(keyframes.get(0).getTime() != 0f)
			keyframes.add(new Keyframe<Float>(0f, dragFrame.getValue()));
		
		dragFrame = null;
		animationButton.repaint();
		repaint();
		fireChangeEvent();
	}

	@Override
	public boolean isKeyframable() {
		return true;
	}


	@Override
	public Object getAnimationValue(float time) {
		if(keyframes.size() == 0)
			if(dragFrame != null)
				return dragFrame.getValue();
			else return 0;
		
		if(time <= keyframes.get(0).getTime())
			return keyframes.get(0).getValue();
		if(time >= keyframes.get(keyframes.size()-1).getTime() || keyframes.size() == 1)
			return keyframes.get(keyframes.size()-1).getValue();
		
		for(int i = 0; i < keyframes.size()-1; i++) {
			if(keyframes.get(i).getTime() <= time && keyframes.get(i + 1).getTime() > time) {
				float distTo0 = time - keyframes.get(i).getTime();
				float distTo1 = keyframes.get(i + 1).getTime() - time;
				float sum = distTo0 + distTo1;
				return (distTo0/sum) * keyframes.get(i+1).getValue() + (distTo1/sum) * keyframes.get(i).getValue(); 
			}
		}
		return 0f;
	}

	@Override
	public Object getStaticValue() {
		return ((Double) manualSpinner.getValue()).floatValue();
	}
	
	@Override
	public String getInstructions() {
		return "This variable is between 0 and 1. Simply draw a graph of how it should change through the animation, or enter a function to start.";
	}
	
	@Override
	public JComponent getManualController() {
		return manualSpinner;
	}
	
	private void clear(float t0, float t1) {
		Collections.sort(keyframes);
		
		for(int i = 0; i < keyframes.size(); i++) {
			float t = keyframes.get(i).getTime();
			if((t >= t0 && t <= t1) || (t >= t1 && t <= t0)) {
				keyframes.remove(i);
				i--;
			}
		}
	}

	@Override
	public Dimension getGraphSize() {
		return new Dimension(500, 300);
	}

	@Override
	public void paintGraph(Graphics2D g, int width, int height) {
		g.setColor(new Color(0, 0, 0, 127));
		
		g.drawString("1.0", 2, g.getFontMetrics().getHeight());
		g.drawString("0.0", 2, height-g.getFontMetrics().getDescent());
		g.setColor(new Color(0, 0, 0, 200));
		
		g.setStroke(new BasicStroke(3f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));
		
		ArrayList<Keyframe<Float>> copy = new ArrayList<>(keyframes);
		if(dragFrame != null)
			copy.add(dragFrame);
		Collections.sort(copy);
		
		if(copy.get(0).getTime() != 0f)
			copy.add(0, new Keyframe<Float>(0f, copy.get(0).getValue()));
		if(copy.get(copy.size()-1).getTime() != 1f)
			copy.add(new Keyframe<Float>(1f, copy.get(copy.size()-1).getValue()));
		
		GeneralPath path = new GeneralPath();
		path.moveTo(copy.get(0).getTime() * width, (1f - copy.get(0).getValue()) * height);
		for(int i = 1; i < copy.size(); i++)
			path.lineTo(copy.get(i).getTime() * width, ((1f - copy.get(i).getValue()) * height-.5f));
		g.draw(path);

		g.setStroke(new BasicStroke(3)); //Draw red bar at each keyframe
		g.setColor(new Color(255, 0, 0, 32));
		for(int i = 1; i < copy.size()-1; i++)
			g.drawLine((int) (copy.get(i).getTime() * width), 0, (int) (copy.get(i).getTime() * width), height);
	}

	@Override
	public void paintButton(Graphics2D g, int width, int height) {
		g.setColor(Color.black);
		g.setStroke(new BasicStroke(1));
		
		GeneralPath path = new GeneralPath();
		path.moveTo(keyframes.get(0).getTime() * width, (1f - keyframes.get(0).getValue()) * height);
		for(int i = 1; i < keyframes.size(); i++)
			path.lineTo(keyframes.get(i).getTime() * width, ((1f - keyframes.get(i).getValue()) * height-.5f));
		g.draw(path);
	}
}