/*******************************************************************************
 * This file is part of logisim-evolution.
 *
 *   logisim-evolution is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   logisim-evolution is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with logisim-evolution.  If not, see <http://www.gnu.org/licenses/>.
 *
 *   Original code by Carl Burch (http://www.cburch.com), 2011.
 *   Subsequent modifications by :
 *     + Haute École Spécialisée Bernoise
 *       http://www.bfh.ch
 *     + Haute École du paysage, d'ingénierie et d'architecture de Genève
 *       http://hepia.hesge.ch/
 *     + Haute École d'Ingénierie et de Gestion du Canton de Vaud
 *       http://www.heig-vd.ch/
 *   The project is currently maintained by :
 *     + REDS Institute - HEIG-VD
 *       Yverdon-les-Bains, Switzerland
 *       http://reds.heig-vd.ch
 *******************************************************************************/

package com.cburch.logisim.std.wiring;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.Icon;
import javax.swing.JDialog;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.text.MaskFormatter;

import com.cburch.logisim.circuit.CircuitState;
import com.cburch.logisim.circuit.RadixOption;
import com.cburch.logisim.comp.EndData;
import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.AttributeOption;
import com.cburch.logisim.data.AttributeSet;
import com.cburch.logisim.data.Attributes;
import com.cburch.logisim.data.BitWidth;
import com.cburch.logisim.data.Bounds;
import com.cburch.logisim.data.Direction;
import com.cburch.logisim.data.Location;
import com.cburch.logisim.data.Value;
import com.cburch.logisim.gui.main.Canvas;
import com.cburch.logisim.instance.Instance;
import com.cburch.logisim.instance.InstanceData;
import com.cburch.logisim.instance.InstanceFactory;
import com.cburch.logisim.instance.InstanceLogger;
import com.cburch.logisim.instance.InstancePainter;
import com.cburch.logisim.instance.InstancePoker;
import com.cburch.logisim.instance.InstanceState;
import com.cburch.logisim.instance.Port;
import com.cburch.logisim.instance.StdAttr;
import com.cburch.logisim.tools.key.BitWidthConfigurator;
import com.cburch.logisim.tools.key.DirectionConfigurator;
import com.cburch.logisim.tools.key.JoinedConfigurator;
import com.cburch.logisim.util.GraphicsUtil;
import com.cburch.logisim.util.Icons;

public class Pin extends InstanceFactory {

	@SuppressWarnings("serial")
	private static class EditText extends JDialog implements KeyListener {

		private Value value = null;
		private Value oldVal = null;
		private JFormattedTextField text = null;
		private int bitWidth;
		RadixOption radix = RadixOption.RADIX_16;

		public EditText(Value value, RadixOption radix, int width) {
			super();
			String mask = "";
			GridBagConstraints gbc = new GridBagConstraints();
			MaskFormatter formatter = new MaskFormatter();
			DecimalFormat df = new DecimalFormat();
			JLabel label = new JLabel("");
			Color back = new Color(0xff, 0xf0, 0x99);

			setUndecorated(true);
			setModal(true);
			setLayout(new GridBagLayout());

			this.radix = radix;
			bitWidth = width;
			oldVal = value;
			// System.err.println("Wdth:"+bitWidth);

			try {
				formatter.setPlaceholderCharacter('_');
				if (radix == RadixOption.RADIX_16) {
					label.setText("0x");
					for (int i = 0; i < Math.ceil(bitWidth / 4.0); i++) {
						mask += "H";
					}
					formatter.setMask(mask);
					text = new JFormattedTextField(formatter);
					text.setText(value.toHexString());
				} else if (radix == RadixOption.RADIX_8) {
					label.setText("0");
					for (int i = 0; i < Math.ceil(bitWidth / 3.0); i++) {
						mask += "#";
					}
					formatter.setInvalidCharacters("89");
					formatter.setMask(mask);
					text = new JFormattedTextField(formatter);
					text.setText(value.toOctalString());
				} else if (radix == RadixOption.RADIX_10_SIGNED) {
					mask = "#;-#";
					df.setParseIntegerOnly(true);
					df.applyPattern(mask);
					df.setMaximumIntegerDigits(11);
					text = new JFormattedTextField(df);
					text.setColumns(11);
					// System.err.println("Val:" + value.toDecimalString(true));
					text.setText(value.toDecimalString(true));
				} else if (radix == RadixOption.RADIX_10_UNSIGNED) {
					mask = "#;";
					df.setParseIntegerOnly(true);
					df.applyPattern(mask);
					df.setMaximumIntegerDigits(10);
					text = new JFormattedTextField(df);
					text.setColumns(10);
					// System.err.println("Val:" +
					// value.toDecimalString(false));
					text.setText(value.toDecimalString(false));
				}
			} catch (ParseException ex) {
				Logger.getLogger(Pin.class.getName()).log(Level.SEVERE, null,
						ex);
			}

			gbc.gridx = gbc.gridy = 0;
			add(label, gbc);
			gbc.gridx = 1;
			gbc.gridwidth = GridBagConstraints.REMAINDER;
			gbc.anchor = GridBagConstraints.BASELINE;
			text.addKeyListener(this);
			text.setBorder(null);
			text.setBackground(back);
			add(text, gbc);

			pack();
		}

		public Value getValue() {
			return value;
		}

		@Override
		public void keyPressed(KeyEvent e) {
			if (e.getKeyCode() == KeyEvent.VK_ENTER) {
				if (text.isEditValid()) {
					if (radix == RadixOption.RADIX_10_SIGNED
							|| radix == RadixOption.RADIX_10_UNSIGNED) {
						try {
							value = Value.createKnown(
									BitWidth.create(bitWidth),
									(int) Long.parseLong(text.getText()));
						} catch (NumberFormatException exception) {
							value = oldVal;
							return;
						}
					} else if (radix == RadixOption.RADIX_16) {
						value = Value.createKnown(BitWidth.create(bitWidth),
								(int) Long.parseLong(text.getText(), 16));
					} else if (radix == RadixOption.RADIX_8) {
						value = Value.createKnown(BitWidth.create(bitWidth),
								(int) Long.parseLong(text.getText(), 8));
					}
					setVisible(false);
				}
			} else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
				value = oldVal;
				setVisible(false);
			}
		}

		@Override
		public void keyReleased(KeyEvent e) {
			;
		}

		@Override
		public void keyTyped(KeyEvent e) {
			;
		}
	}

	public static class PinLogger extends InstanceLogger {

		@Override
		public String getLogName(InstanceState state, Object option) {
			PinAttributes attrs = (PinAttributes) state.getAttributeSet();
			String ret = attrs.label;
			if (ret == null || ret.equals("")) {
				String type = attrs.type == EndData.INPUT_ONLY ? Strings
						.get("pinInputName") : Strings.get("pinOutputName");
				return type + state.getInstance().getLocation();
			} else {
				return ret;
			}
		}

		@Override
		public Value getLogValue(InstanceState state, Object option) {
			PinState s = getState(state);
			return s.intendedValue;
		}
	}

	public static class PinPoker extends InstancePoker {

		int bitPressed = -1;

		private int getBit(InstanceState state, MouseEvent e) {
			BitWidth width = state.getAttributeValue(StdAttr.WIDTH);
			if (width.getWidth() == 1) {
				return 0;
			} else {
				Bounds bds = state.getInstance().getBounds(); // intentionally
				// with no
				// graphics
				// object - we
				// don't want
				// label
				// included
				int i = (bds.getX() + bds.getWidth() - e.getX()) / 10;
				int j = (bds.getY() + bds.getHeight() - e.getY()) / 20;
				int bit = 8 * j + i;
				if (bit < 0 || bit >= width.getWidth()) {
					return -1;
				} else {
					return bit;
				}
			}
		}

		private void handleBitPress(InstanceState state, int bit, MouseEvent e) {
			PinAttributes attrs = (PinAttributes) state.getAttributeSet();
			if (!attrs.isInput()) {
				return;
			}

			java.awt.Component sourceComp = e.getComponent();
			if (sourceComp instanceof Canvas && !state.isCircuitRoot()) {
				Canvas canvas = (Canvas) e.getComponent();
				CircuitState circState = canvas.getCircuitState();
				java.awt.Component frame = SwingUtilities.getRoot(canvas);
				int choice = JOptionPane.showConfirmDialog(frame,
						Strings.get("pinFrozenQuestion"),
						Strings.get("pinFrozenTitle"),
						JOptionPane.OK_CANCEL_OPTION,
						JOptionPane.WARNING_MESSAGE);
				if (choice == JOptionPane.OK_OPTION) {
					circState = circState.cloneState();
					canvas.getProject().setCircuitState(circState);
					state = circState.getInstanceState(state.getInstance());
				} else {
					return;
				}
			}

			PinState pinState = getState(state);
			Value val = pinState.intendedValue.get(bit);
			if (val == Value.FALSE) {
				val = Value.TRUE;
			} else if (val == Value.TRUE) {
				val = attrs.threeState && attrs.pull == PULL_NONE ? Value.UNKNOWN
						: Value.FALSE;
			} else {
				val = Value.FALSE;
			}
			pinState.intendedValue = pinState.intendedValue.set(bit, val);
			state.fireInvalidated();
		}

		@Override
		public void mousePressed(InstanceState state, MouseEvent e) {
			bitPressed = getBit(state, e);
		}

		@Override
		public void mouseReleased(InstanceState state, MouseEvent e) {
			if (state.getAttributeValue(RadixOption.ATTRIBUTE) == RadixOption.RADIX_2) {
				int bit = getBit(state, e);
				if (bit == bitPressed && bit >= 0) {
					handleBitPress(state, bit, e);
				}
				bitPressed = -1;
			} else {
				PinState pinState = getState(state);
				EditText dialog = new EditText(pinState.intendedValue,
						state.getAttributeValue(RadixOption.ATTRIBUTE),
						pinState.intendedValue.getWidth());
				dialog.setLocation(e.getXOnScreen(), e.getYOnScreen());
				dialog.setVisible(true);
				// System.err.println("New Value: '" + dialog.getValue() + "'");
				pinState.intendedValue = dialog.getValue();
				state.fireInvalidated();
			}
		}
	}

	private static class PinState implements InstanceData, Cloneable {

		Value intendedValue;
		Value foundValue;

		public PinState(Value sending, Value receiving) {
			this.intendedValue = sending;
			this.foundValue = receiving;
		}

		@Override
		public Object clone() {
			try {
				return super.clone();
			} catch (CloneNotSupportedException e) {
				return null;
			}
		}
	}

	private static PinState getState(InstanceState state) {
		PinAttributes attrs = (PinAttributes) state.getAttributeSet();
		BitWidth width = attrs.width;
		PinState ret = (PinState) state.getData();
		if (ret == null) {
			Value val = attrs.threeState ? Value.UNKNOWN : Value.FALSE;
			if (width.getWidth() > 1) {
				Value[] arr = new Value[width.getWidth()];
				java.util.Arrays.fill(arr, val);
				val = Value.create(arr);
			}
			ret = new PinState(val, val);
			state.setData(ret);
		}
		if (ret.intendedValue.getWidth() != width.getWidth()) {
			ret.intendedValue = ret.intendedValue.extendWidth(width.getWidth(),
					attrs.threeState ? Value.UNKNOWN : Value.FALSE);
		}
		if (ret.foundValue.getWidth() != width.getWidth()) {
			ret.foundValue = ret.foundValue.extendWidth(width.getWidth(),
					Value.UNKNOWN);
		}
		return ret;
	}

	private static Value pull2(Value mod, BitWidth expectedWidth, Value pullTo) {
		if (mod.getWidth() == expectedWidth.getWidth()) {
			Value[] vs = mod.getAll();
			for (int i = 0; i < vs.length; i++) {
				if (vs[i] == Value.UNKNOWN) {
					vs[i] = pullTo;
				}
			}
			return Value.create(vs);
		} else {
			return Value.createKnown(expectedWidth, 0);
		}
	}

	public static final Attribute<Boolean> ATTR_TRISTATE = Attributes
			.forBoolean("tristate", Strings.getter("pinThreeStateAttr"));
	public static final Attribute<Boolean> ATTR_TYPE = Attributes.forBoolean(
			"output", Strings.getter("pinOutputAttr"));
	public static final Attribute<Direction> ATTR_LABEL_LOC = Attributes
			.forDirection("labelloc", Strings.getter("pinLabelLocAttr"));
	public static final AttributeOption PULL_NONE = new AttributeOption("none",
			Strings.getter("pinPullNoneOption"));
	public static final AttributeOption PULL_UP = new AttributeOption("up",
			Strings.getter("pinPullUpOption"));
	public static final AttributeOption PULL_DOWN = new AttributeOption("down",
			Strings.getter("pinPullDownOption"));

	public static final Attribute<AttributeOption> ATTR_PULL = Attributes
			.forOption("pull", Strings.getter("pinPullAttr"),
					new AttributeOption[] { PULL_NONE, PULL_UP, PULL_DOWN });

	public static final Pin FACTORY = new Pin();

	private static final Icon ICON_IN = Icons.getIcon("pinInput.gif");

	private static final Icon ICON_OUT = Icons.getIcon("pinOutput.gif");

	private static final Font ICON_WIDTH_FONT = new Font("SansSerif",
			Font.BOLD, 9);

	private static final Color ICON_WIDTH_COLOR = Value.WIDTH_ERROR_COLOR
			.darker();

	public Pin() {
		super("Pin", Strings.getter("pinComponent"));
		setFacingAttribute(StdAttr.FACING);
		setKeyConfigurator(JoinedConfigurator.create(new BitWidthConfigurator(
				StdAttr.WIDTH), new DirectionConfigurator(ATTR_LABEL_LOC,
				KeyEvent.ALT_DOWN_MASK)));
		setInstanceLogger(PinLogger.class);
		setInstancePoker(PinPoker.class);
	}

	//
	// methods for instances
	//
	@Override
	protected void configureNewInstance(Instance instance) {
		PinAttributes attrs = (PinAttributes) instance.getAttributeSet();
		instance.addAttributeListener();
		configurePorts(instance);
		Probe.configureLabel(instance, attrs.labelloc, attrs.facing);
	}

	private void configurePorts(Instance instance) {
		PinAttributes attrs = (PinAttributes) instance.getAttributeSet();
		String endType = attrs.isOutput() ? Port.INPUT : Port.OUTPUT;
		Port port = new Port(0, 0, endType, StdAttr.WIDTH);
		if (attrs.isOutput()) {
			port.setToolTip(Strings.getter("pinOutputToolTip"));
		} else {
			port.setToolTip(Strings.getter("pinInputToolTip"));
		}
		instance.setPorts(new Port[] { port });
	}

	@Override
	public AttributeSet createAttributeSet() {
		return new PinAttributes();
	}

	@Override
	public Bounds getOffsetBounds(AttributeSet attrs) {
		Direction facing = attrs.getValue(StdAttr.FACING);
		BitWidth width = attrs.getValue(StdAttr.WIDTH);
		return Probe.getOffsetBounds(facing, width,
				attrs.getValue(RadixOption.ATTRIBUTE) /* RadixOption.RADIX_2 */);
	}

	public int getType(Instance instance) {
		PinAttributes attrs = (PinAttributes) instance.getAttributeSet();
		return attrs.type;
	}

	//
	// state information methods
	//
	public Value getValue(InstanceState state) {
		return getState(state).intendedValue;
	}

	//
	// basic information methods
	//
	public BitWidth getWidth(Instance instance) {
		PinAttributes attrs = (PinAttributes) instance.getAttributeSet();
		return attrs.width;
	}

	@Override
	public boolean HasThreeStateDrivers(AttributeSet attrs) {
		/*
		 * We ignore for the moment the three-state property of the pin, as it
		 * is not an active component, just wiring
		 */
		// PinAttributes myattrs = (PinAttributes) attrs;
		// return myattrs.getValue(Pin.ATTR_TRISTATE);
		return false;
	}

	@Override
	public boolean HDLSupportedComponent(String HDLIdentifier,
			AttributeSet attrs) {
		return true;
	}

	@Override
	protected void instanceAttributeChanged(Instance instance, Attribute<?> attr) {
		if (attr == ATTR_TYPE) {
			configurePorts(instance);
		} else if (attr == StdAttr.WIDTH || attr == StdAttr.FACING
				|| attr == Pin.ATTR_LABEL_LOC || attr == RadixOption.ATTRIBUTE) {
			instance.recomputeBounds();
			PinAttributes attrs = (PinAttributes) instance.getAttributeSet();
			Probe.configureLabel(instance, attrs.labelloc, attrs.facing);
		} else if (attr == Pin.ATTR_TRISTATE || attr == Pin.ATTR_PULL) {
			instance.fireInvalidated();
		}
	}

	public boolean isInputPin(Instance instance) {
		PinAttributes attrs = (PinAttributes) instance.getAttributeSet();
		return attrs.type != EndData.OUTPUT_ONLY;
	}

	@Override
	public void paintGhost(InstancePainter painter) {
		PinAttributes attrs = (PinAttributes) painter.getAttributeSet();
		Location loc = painter.getLocation();
		Bounds bds = painter.getOffsetBounds();
		int x = loc.getX();
		int y = loc.getY();
		Graphics g = painter.getGraphics();
		GraphicsUtil.switchToWidth(g, 2);
		boolean output = attrs.isOutput();
		if (output) {
			BitWidth width = attrs.getValue(StdAttr.WIDTH);
			if (width == BitWidth.ONE) {
				g.drawOval(x + bds.getX() + 1, y + bds.getY() + 1,
						bds.getWidth() - 1, bds.getHeight() - 1);
			} else {
				g.drawRoundRect(x + bds.getX() + 1, y + bds.getY() + 1,
						bds.getWidth() - 1, bds.getHeight() - 1, 6, 6);
			}
		} else {
			g.drawRect(x + bds.getX() + 1, y + bds.getY() + 1,
					bds.getWidth() - 1, bds.getHeight() - 1);
		}
	}

	//
	// graphics methods
	//
	@Override
	public void paintIcon(InstancePainter painter) {
		paintIconBase(painter);
		BitWidth w = painter.getAttributeValue(StdAttr.WIDTH);
		if (!w.equals(BitWidth.ONE)) {
			Graphics g = painter.getGraphics();
			g.setColor(ICON_WIDTH_COLOR);
			g.setFont(ICON_WIDTH_FONT);
			GraphicsUtil.drawCenteredText(g, "" + w.getWidth(), 10, 9);
			g.setColor(Color.BLACK);
		}
	}

	private void paintIconBase(InstancePainter painter) {
		PinAttributes attrs = (PinAttributes) painter.getAttributeSet();
		Direction dir = attrs.facing;
		boolean output = attrs.isOutput();
		Graphics g = painter.getGraphics();
		if (output) {
			if (ICON_OUT != null) {
				Icons.paintRotated(g, 2, 2, dir, ICON_OUT,
						painter.getDestination());
				return;
			}
		} else {
			if (ICON_IN != null) {
				Icons.paintRotated(g, 2, 2, dir, ICON_IN,
						painter.getDestination());
				return;
			}
		}
		int pinx = 16;
		int piny = 9;
		if (dir == Direction.EAST) { // keep defaults
		} else if (dir == Direction.WEST) {
			pinx = 4;
		} else if (dir == Direction.NORTH) {
			pinx = 9;
			piny = 4;
		} else if (dir == Direction.SOUTH) {
			pinx = 9;
			piny = 16;
		}

		g.setColor(Color.black);
		if (output) {
			g.drawOval(4, 4, 13, 13);
		} else {
			g.drawRect(4, 4, 13, 13);
		}
		g.setColor(Value.TRUE.getColor());
		g.fillOval(7, 7, 8, 8);
		g.fillOval(pinx, piny, 3, 3);
	}

	@Override
	public void paintInstance(InstancePainter painter) {
		PinAttributes attrs = (PinAttributes) painter.getAttributeSet();
		Graphics g = painter.getGraphics();
		Bounds bds = painter.getInstance().getBounds(); // intentionally with no
		// graphics object - we
		// don't want label
		// included
		int x = bds.getX();
		int y = bds.getY();
		GraphicsUtil.switchToWidth(g, 2);
		g.setColor(Color.black);
		if (attrs.type == EndData.OUTPUT_ONLY) {
			if (attrs.width.getWidth() == 1) {
				g.drawOval(x + 1, y + 1, bds.getWidth() - 1,
						bds.getHeight() - 1);
			} else {
				g.drawRoundRect(x + 1, y + 1, bds.getWidth() - 1,
						bds.getHeight() - 1, 12, 12);
			}
		} else {
			g.drawRect(x + 1, y + 1, bds.getWidth() - 1, bds.getHeight() - 1);
		}

		painter.drawLabel();

		if (!painter.getShowState()) {
			g.setColor(Color.BLACK);
			GraphicsUtil.drawCenteredText(g, "x" + attrs.width.getWidth(),
					bds.getX() + bds.getWidth() / 2,
					bds.getY() + bds.getHeight() / 2);
		} else {
			PinState state = getState(painter);
			if (attrs.width.getWidth() <= 1) {
				Value found = state.foundValue;
				g.setColor(found.getColor());
				g.fillOval(x + 4, y + 4, 13, 13);

				if (attrs.width.getWidth() == 1) {
					g.setColor(Color.WHITE);
					GraphicsUtil.drawCenteredText(g,
							state.intendedValue.toDisplayString(), x + 11,
							y + 9);
				}
			} else {
				Probe.paintValue(painter, state.intendedValue);
			}
		}

		painter.drawPorts();
	}

	@Override
	public void propagate(InstanceState state) {
		PinAttributes attrs = (PinAttributes) state.getAttributeSet();

		PinState q = getState(state);
		if (attrs.type == EndData.OUTPUT_ONLY) {
			Value found = state.getPortValue(0);
			q.intendedValue = found;
			q.foundValue = found;
			state.setPort(0, Value.createUnknown(attrs.width), 1);
		} else {
			Value found = state.getPortValue(0);
			Value toSend = q.intendedValue;

			Object pull = attrs.pull;
			Value pullTo = null;
			if (pull == PULL_DOWN) {
				pullTo = Value.FALSE;
			} else if (pull == PULL_UP) {
				pullTo = Value.TRUE;
			} else if (!attrs.threeState && !state.isCircuitRoot()) {
				pullTo = Value.FALSE;
			}
			if (pullTo != null) {
				toSend = pull2(toSend, attrs.width, pullTo);
				if (state.isCircuitRoot()) {
					q.intendedValue = toSend;
				}
			}

			q.foundValue = found;
			if (!toSend.equals(found)) { // ignore if no change
				state.setPort(0, toSend, 1);
			}
		}
	}

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

	public void setValue(InstanceState state, Value value) {
		PinAttributes attrs = (PinAttributes) state.getAttributeSet();
		Object pull = attrs.pull;

		PinState myState = getState(state);
		if (value == Value.NIL) {
			myState.intendedValue = Value.createUnknown(attrs.width);
		} else {
			Value sendValue;
			if (pull == PULL_NONE || pull == null || value.isFullyDefined()) {
				sendValue = value;
			} else {
				Value[] bits = value.getAll();
				if (pull == PULL_UP) {
					for (int i = 0; i < bits.length; i++) {
						if (bits[i] != Value.FALSE)
							bits[i] = Value.TRUE;
					}
				} else if (pull == PULL_DOWN) {
					for (int i = 0; i < bits.length; i++) {
						if (bits[i] != Value.TRUE)
							bits[i] = Value.FALSE;
					}
				}
				sendValue = Value.create(bits);
			}
			myState.intendedValue = sendValue;
		}
	}

}