/**
 * CertainTrust SDK
 * 
 * Implements the computational trust model "CertainTrust"
 * in Java.
 * See <http://www.tk.informatik.tu-darmstadt.de/de/research/smart-security-and-trust/> for further details.
 * 
 * 
 * Telecooperation Department, Technische Universität Darmstadt
 * <http://www.tk.informatik.tu-darmstadt.de/>
 * 
 * Prof. Dr. Max Mühlhäuser <max@informatik.tu-darmstadt.de>
 * Florian Volk <florian.volk@cased.de>
 * 
 * 
 * @author	David Kalnischkies
 * @author	Florian Volk
 * @version	1.0
 */

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


package CertainTrust;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;

import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.border.EmptyBorder;

/**
 * The CertainTrustHTI includes a formular to change
 * the C, T and F values of the given CertainTrust
 * element and a canvas displaying the expectation
 * calculated from these values
 */
public class CertainTrustHTI extends JPanel implements Observer {
	private static final long serialVersionUID = -1194437596328826222L;

	/**
	 * Creates a new HTI to display a CertainTrust element
	 * 
	 * @param certainlogic element to display
	 */
	public CertainTrustHTI(CertainTrust certainlogic) {
		setup(certainlogic, null);
	}
	
	/**
	 * Creates a new HTI to display a CertainTrust element
	 * 
	 * Supported config options are:
	 * - label.lang: language for the text elements (available: "de" and "en" (default))
	 * - label.f, label.c, label.t and label.e to set the text explicitly
	 * - readonly: to effect the complete HTI (value either "true" or "false") and
	 * - readonly.f, readonly.c, readonly.t, readonly.e to set explicitly
	 * See also {@link CertainTrustCanvas#CertainTrustCanvas(CertainTrust, Map)} for
	 * additional options which are accepted by the canvas element
	 * 
	 * @param certainlogic element to display
	 * @param config to use for this HTI
	 */
	public CertainTrustHTI(CertainTrust certainlogic, Map<String, String> config) {
		setup(certainlogic, config);
	}
	
	private CertainTrust cl;
	private JTextField f;
	private JTextField c;
	private JTextField t;
	private JTextField e;
	private CertainTrustCanvas ctc;
	private Map<String,String> config;
	
	protected CertainTrustCanvas getCanvas() { return ctc; }
	
	private static JTextField appendInput(JPanel p, String name, char mnemonic, double value, String readOnly, CertainTrustActionFocusListener listener) {
		JLabel l = new JLabel(name);
		l.setBorder(new EmptyBorder(0, 0, 0, (int) (l.getFont().getSize() * 1.5)));
		final JTextField f = new JTextField(CertainTrustHTI.formatFloat(value), 4);
		if (readOnly.compareToIgnoreCase("true") == 0) {
			f.setEditable(false);
			f.setFocusable(false);
		}
		else if (readOnly.compareToIgnoreCase("false") != 0)
			throw new IllegalArgumentException("Input element " + name + " has no valid readonly setting");
		else if (listener != null) {
			f.addActionListener(listener);
			f.addFocusListener(listener);
		}
		l.setLabelFor(f);
		if (mnemonic != '\0')
			l.setDisplayedMnemonic(mnemonic);
		p.add(l);
		p.add(f);
		return f;
	}
	
	private abstract class CertainTrustActionFocusListener implements ActionListener, FocusListener {
		@Override
		public void focusGained(FocusEvent arg0) {}

		@Override
		public void focusLost(FocusEvent arg0) {
			update();
		}

		@Override
		public void actionPerformed(ActionEvent arg0) {
			update();
		}
		
		abstract void update();
		
		/**
		 * parseInput tries to convert a textual input into a floating point number in [0;1]
		 * It accepts both "." and "," as decimal delimiters.
		 * If the conversion fails, a default value is used.
		 * @param input	the textual input to convert into a floating point number
		 * @param defaultValue	the default value used in case the conversion fails
		 * @return	the floating point value generated from the textual input
		 */
		protected double parseInput(String input, double defaultValue) {
			double result;
			try {
				// first, replace the first "," with a "." to understand German-language style floating point notation
				String rawInput = input.replaceFirst(",", ".");
				
				result = Double.parseDouble(rawInput);
				
				// check, if the value in within our bounds of [0;1]
				if ((1 < result) || (0 > result)) {
					result = defaultValue;
				}
			}
			catch (NumberFormatException e) {
				result = defaultValue;
			}
			return result;
		}
	}
	
	private void setup(CertainTrust certainlogic, Map<String,String> config) {
		cl = certainlogic;
		if (config != null)
			this.config = config;
		else
			this.config = new HashMap<String,String>();
		
		if (this.config.containsKey("label.lang") == false)
			this.config.put("label.lang", "en");
		if (this.config.get("label.lang") == "de") {
			if (this.config.containsKey("label.f") == false)
				this.config.put("label.f", "Initialwert");
			if (this.config.containsKey("label.t") == false)
				this.config.put("label.t", "Vertrauen");
			if (this.config.containsKey("label.c") == false)
				this.config.put("label.c", "Sicherheit");
			if (this.config.containsKey("label.e") == false)
				this.config.put("label.e", "Erwartung");
		} else {
			if (this.config.containsKey("label.f") == false)
				this.config.put("label.f", "Init. value");
			if (this.config.containsKey("label.t") == false)
				this.config.put("label.t", "Trust");
			if (this.config.containsKey("label.c") == false)
				this.config.put("label.c", "Certainty");
			if (this.config.containsKey("label.e") == false)
				this.config.put("label.e", "Expectation");
		}
		
		if (this.config.containsKey("readonly") == false) {
			if (this.config.containsKey("readonly.f") == false)
				this.config.put("readonly.f", "false");
			if (this.config.containsKey("readonly.t") == false)
				this.config.put("readonly.t", "false");
			if (this.config.containsKey("readonly.c") == false)
				this.config.put("readonly.c", "false");
			if (this.config.containsKey("readonly.e") == false)
				this.config.put("readonly.e", "true");
		} else {
			String readOnly = this.config.get("readonly");
			if (this.config.containsKey("readonly.f") == false)
				this.config.put("readonly.f", readOnly);
			if (this.config.containsKey("readonly.t") == false)
				this.config.put("readonly.t", readOnly);
			if (this.config.containsKey("readonly.c") == false)
				this.config.put("readonly.c", readOnly);
			if (this.config.containsKey("readonly.e") == false)
				this.config.put("readonly.e", readOnly);
		}
		
		JPanel inputs = new JPanel(new GridLayout(5,2));
		f = appendInput(inputs, this.config.get("label.f"), 'f', cl.getF(), this.config.get("readonly.f"),
				new CertainTrustActionFocusListener() {
					@Override
					void update() {
						cl.setF(this.parseInput(f.getText(), cl.getF()));
					}
				});
		t = appendInput(inputs, this.config.get("label.t"), 't', cl.getT(), this.config.get("readonly.t"),
				new CertainTrustActionFocusListener() {
			@Override
			void update() {
				cl.setTC(this.parseInput(t.getText(), cl.getT()),
						cl.getC());
			}
		});
		c = appendInput(inputs, this.config.get("label.c"), 'c', cl.getC(), this.config.get("readonly.c"),
				new CertainTrustActionFocusListener() {
			@Override
			void update() {
				cl.setTC(cl.getT(),
						this.parseInput(c.getText(), cl.getC()));
			}
		});
		JLabel ignore0 = new JLabel("");
		inputs.add(ignore0);
		JLabel ignore1 = new JLabel("");
		inputs.add(ignore1);
		e = appendInput(inputs, this.config.get("label.e"),'\0', cl.getExpectation(), this.config.get("readonly.e"), null);
		this.add(inputs);
		
		JPanel jp = new JPanel();
		jp.setLayout(new BoxLayout(jp, BoxLayout.PAGE_AXIS));
		JPanel jp1 = new JPanel();
		JLabel certain = new JLabel(this.config.get("label.c") + "\u00a0\u2192");
		certain.setUI(new VerticalLabelUI());
		jp1.add(certain);
		ctc = new CertainTrustCanvas(cl, this.config);
		ctc.repaint();
		jp1.add(ctc);
		JPanel jp2 = new JPanel(new BorderLayout());
		JLabel origin = new JLabel("0");
		int fontsize = certain.getFont().getSize();
		origin.setBorder(new EmptyBorder(0, fontsize, 0, fontsize));
		jp2.add(origin, BorderLayout.LINE_START);
		JLabel trust = new JLabel(this.config.get("label.t") + "\u00a0\u2192");
		jp2.add(trust, BorderLayout.CENTER);
		jp.add(jp1);
		jp.add(jp2);
		
		this.add(jp);
		this.setVisible(true);
		cl.addObserver(this);
	}

	@Override
	public void update(Observable ignored0, Object ignored1) {
		f.setText(CertainTrustHTI.formatFloat(cl.getF()));
		t.setText(CertainTrustHTI.formatFloat(cl.getT()));
		c.setText(CertainTrustHTI.formatFloat(cl.getC()));
		e.setText(CertainTrustHTI.formatFloat(cl.getExpectation()));
	}
	
	/**
	 * Rounds a number to at most 3 decimal places and converts it to a string
	 * @param value	the number to round
	 * @return	the rounded value as string
	 */
	static private String formatFloat(double value) {
		// the explicit type cast from Long to double is required, otherwise rounded will always be 0
		double rounded = ((double)Math.round(value * 1000) / 1000);
		
		// Use NumerFormat to omit trailing 0s as those which Double.toString() produces
		NumberFormat formatter = new DecimalFormat("0.###");
		String result = formatter.format(rounded);
		
		// for compatibility with the JS version, our decimal delimiter *always* is "."
		result = result.replaceFirst(",", ".");
		
		return result;
	}
}
