/* Copyright (c) 2010, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */

package com.cburch.logisim.std.gates;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.util.Map;

import javax.swing.Icon;

import com.cburch.logisim.LogisimVersion;
import com.cburch.logisim.analyze.model.Expression;
import com.cburch.logisim.analyze.model.Expressions;
import com.cburch.logisim.circuit.ExpressionComputer;
import com.cburch.logisim.comp.TextField;
import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.AttributeSet;
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.file.Options;
import com.cburch.logisim.instance.Instance;
import com.cburch.logisim.instance.InstanceFactory;
import com.cburch.logisim.instance.InstancePainter;
import com.cburch.logisim.instance.InstanceState;
import com.cburch.logisim.instance.Port;
import com.cburch.logisim.instance.StdAttr;
import com.cburch.logisim.prefs.AppPreferences;
import com.cburch.logisim.tools.WireRepair;
import com.cburch.logisim.tools.WireRepairData;
import com.cburch.logisim.tools.key.BitWidthConfigurator;
import com.cburch.logisim.tools.key.IntegerConfigurator;
import com.cburch.logisim.tools.key.JoinedConfigurator;
import com.cburch.logisim.util.GraphicsUtil;
import com.cburch.logisim.util.Icons;
import com.cburch.logisim.util.StringGetter;

abstract class AbstractGate extends InstanceFactory {
	private String[] iconNames = new String[3];
	private Icon[] icons = new Icon[3];
	private int bonusWidth = 0;
	private boolean negateOutput = false;
	private boolean isXor = false;
	private String rectLabel = "";
	private boolean paintInputLines;

	protected AbstractGate(String name, StringGetter desc) {
		this(name, desc, false);
	}
	
	protected AbstractGate(String name, StringGetter desc, boolean isXor) {
		super(name, desc);
		this.isXor = isXor;
		setFacingAttribute(StdAttr.FACING);
		setKeyConfigurator(JoinedConfigurator.create(   
			new IntegerConfigurator(GateAttributes.ATTR_INPUTS, 2,
					GateAttributes.MAX_INPUTS, 0),
			new BitWidthConfigurator(StdAttr.WIDTH)));
	}
	

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

	@Override
	public Object getDefaultAttributeValue(Attribute<?> attr, LogisimVersion ver) {
		if (attr instanceof NegateAttribute) {
			return Boolean.FALSE;
		} else {
			return super.getDefaultAttributeValue(attr, ver);
		}
	}

	@Override
	public Bounds getOffsetBounds(AttributeSet attrsBase) {
		GateAttributes attrs = (GateAttributes) attrsBase;
		Direction facing = attrs.facing;
		int size = ((Integer) attrs.size.getValue()).intValue();
		int inputs = attrs.inputs;
		if (inputs % 2 == 0) {
			inputs++;
		}
		int negated = attrs.negated;
		
		int width = size + bonusWidth + (negateOutput ? 10 : 0);
		if (negated != 0) {
			width += 10;
		}
		int height = Math.max(10 * inputs, size);
		if (facing == Direction.SOUTH) {
			return Bounds.create(-height / 2, -width, height, width);
		} else if (facing == Direction.NORTH) {
			return Bounds.create(-height / 2, 0, height, width);
		} else if (facing == Direction.WEST) {
			return Bounds.create(0, -height / 2, width, height);
		} else {
			return Bounds.create(-width, -height / 2, width, height);
		}
	}
	
	@Override
	public boolean contains(Location loc, AttributeSet attrsBase) {
		GateAttributes attrs = (GateAttributes) attrsBase;
		if (super.contains(loc, attrs)) {
			if (attrs.negated == 0) {
				return true;
			} else {
				Direction facing = attrs.facing;
				Bounds bds = getOffsetBounds(attrsBase);
				int delt;
				if (facing == Direction.NORTH) {
					delt = loc.getY() - (bds.getY() + bds.getHeight());
				} else if (facing == Direction.SOUTH) {
					delt = loc.getY() - bds.getY();
				} else if (facing == Direction.WEST) {
					delt = loc.getX() - (bds.getX() + bds.getHeight());
				} else {
					delt = loc.getX() - bds.getX();
				}
				if (Math.abs(delt) > 5) {
					return true;
				} else {
					int inputs = attrs.inputs;
					for (int i = 1; i <= inputs; i++) {
						Location offs = getInputOffset(attrs, i);
						if (loc.manhattanDistanceTo(offs) <= 5) return true;
					}
					return false;
				}
			}
		} else {
			return false;
		}
	}

	//
	// painting methods
	//
	@Override
	public void paintGhost(InstancePainter painter) {
		paintBase(painter);
	}

	@Override
	public void paintInstance(InstancePainter painter) {
		paintBase(painter);
		if (!painter.isPrintView() || painter.getGateShape() == AppPreferences.SHAPE_RECTANGULAR) {
			painter.drawPorts();
		}
	}
	
	private void paintBase(InstancePainter painter) {
		GateAttributes attrs = (GateAttributes) painter.getAttributeSet();
		Direction facing = attrs.facing;
		int inputs = attrs.inputs;
		int negated = attrs.negated;

		Object shape = painter.getGateShape();
		Location loc = painter.getLocation();
		Bounds bds = painter.getOffsetBounds();
		int width = bds.getWidth();
		int height = bds.getHeight();
		if (facing == Direction.NORTH || facing == Direction.SOUTH) {
			int t = width; width = height; height = t;
		}
		if (negated != 0) {
			width -= 10;
		}

		Graphics g = painter.getGraphics();
		Color baseColor = g.getColor();
		if (shape == AppPreferences.SHAPE_SHAPED && paintInputLines) {
			PainterShaped.paintInputLines(painter, this);
		} else if (negated != 0) {
			for (int i = 0; i < inputs; i++) {
				int negatedBit = (negated >> i) & 1;
				if (negatedBit == 1) {
					Location in = getInputOffset(attrs, i);
					Location cen = in.translate(facing, 5);
					painter.drawDongle(loc.getX() + cen.getX(),
							loc.getY() + cen.getY());
				}
			}
		}
		
		g.setColor(baseColor);
		g.translate(loc.getX(), loc.getY());
		double rotate = 0.0;
		if (facing != Direction.EAST && g instanceof Graphics2D) {
			rotate = -facing.toRadians();
			Graphics2D g2 = (Graphics2D) g;
			g2.rotate(rotate);
		}
		
		if (shape == AppPreferences.SHAPE_RECTANGULAR) {
			paintRectangular(painter, width, height);
		} else if (shape == AppPreferences.SHAPE_DIN40700) {
			paintDinShape(painter, width, height, inputs);
		} else { // SHAPE_SHAPED
			if (negateOutput) {
				g.translate(-10, 0);
				paintShape(painter, width - 10, height);
				painter.drawDongle(5, 0);
				g.translate(10, 0);
			} else {
				paintShape(painter, width, height);
			}
		}
		
		if (rotate != 0.0) {
			((Graphics2D) g).rotate(-rotate);
		}
		g.translate(-loc.getX(), -loc.getY());
		
		painter.drawLabel();
	}

	protected void setIconNames(String all) {
		setIconNames(all, all, all);
	}
	
	protected void setIconNames(String shaped, String rect, String din) {
		iconNames[0] = shaped;
		iconNames[1] = rect;
		iconNames[2] = din;
	}
	
	private Icon getIcon(int type) {
		Icon ret = icons[type];
		if (ret != null) {
			return ret;
		} else {
			String iconName = iconNames[type];
			if (iconName == null) {
				return null;
			} else {
				ret = Icons.getIcon(iconName);
				if (ret == null) {
					iconNames[type] = null;
				} else {
					icons[type] = ret;
				}
				return ret;
			}
		}
	}
	
	private Icon getIconShaped() {
		return getIcon(0);
	}
	
	private Icon getIconRectangular() {
		return getIcon(1);
	}
	
	private Icon getIconDin40700() {
		return getIcon(2);
	}
	
	protected void setPaintInputLines(boolean value) {
		paintInputLines = value;
	}
	
	protected abstract void paintIconShaped(InstancePainter painter);
	
	protected void paintIconRectangular(InstancePainter painter) {
		Graphics g = painter.getGraphics();
		g.drawRect(1, 2, 16, 16);
		if (negateOutput) g.drawOval(16, 8, 4, 4);
		String label = getRectangularLabel(painter.getAttributeSet());
		GraphicsUtil.drawCenteredText(g, label, 9, 8);
	}

	@Override
	public final void paintIcon(InstancePainter painter) {
		Graphics g = painter.getGraphics();
		g.setColor(Color.black);
		if (painter.getGateShape() == AppPreferences.SHAPE_RECTANGULAR) {
			Icon iconRect = getIconRectangular();
			if (iconRect != null) {
				iconRect.paintIcon(painter.getDestination(), g, 2, 2);
			} else {
				paintIconRectangular(painter);
			}
		} else if (painter.getGateShape() == AppPreferences.SHAPE_DIN40700) {
			Icon iconDin = getIconDin40700();
			if (iconDin != null) {
				iconDin.paintIcon(painter.getDestination(), g, 2, 2);
			} else {
				paintIconRectangular(painter);
			}
		} else {
			Icon iconShaped = getIconShaped();
			if (iconShaped != null) {
				iconShaped.paintIcon(painter.getDestination(), g, 2, 2);
			} else {
				paintIconShaped(painter);
			}
		}
	}

	protected void setAdditionalWidth(int value) {
		bonusWidth = value;
	}

	protected void setNegateOutput(boolean value) {
		negateOutput = value;
	}

	protected void setRectangularLabel(String value) {
		rectLabel = value;
	}

	protected String getRectangularLabel(AttributeSet attrs) {
		return rectLabel;
	}

	//
	// protected methods intended to be overridden
	//
	protected abstract Value getIdentity();

	protected abstract void paintShape(InstancePainter painter,
			int width, int height);

	protected void paintRectangular(InstancePainter painter,
			int width, int height) {
		int don = negateOutput ? 10 : 0;
		AttributeSet attrs = painter.getAttributeSet();
		painter.drawRectangle(-width, -height / 2, width - don, height,
				getRectangularLabel(attrs));
		if (negateOutput) {
			painter.drawDongle(-5, 0);
		}
	}

	protected abstract void paintDinShape(InstancePainter painter,
			int width, int height, int inputs);
	
	protected abstract Value computeOutput(Value[] inputs, int numInputs,
			InstanceState state);
	
	protected abstract Expression computeExpression(Expression[] inputs,
			int numInputs);

	protected boolean shouldRepairWire(Instance instance, WireRepairData data) {
		return false;
	}

	//
	// methods for instances
	//
	@Override
	protected void configureNewInstance(Instance instance) {
		instance.addAttributeListener();
		computePorts(instance);
		computeLabel(instance);
	}
	
	@Override
	protected void instanceAttributeChanged(Instance instance, Attribute<?> attr) {
		if (attr == GateAttributes.ATTR_SIZE || attr == StdAttr.FACING) {
			instance.recomputeBounds();
			computePorts(instance);
			computeLabel(instance);
		} else if (attr == GateAttributes.ATTR_INPUTS
				|| attr instanceof NegateAttribute) {
			instance.recomputeBounds();
			computePorts(instance);
		} else if (attr == GateAttributes.ATTR_XOR) {
			instance.fireInvalidated();
		}
	}
	
	private void computeLabel(Instance instance) {
		GateAttributes attrs = (GateAttributes) instance.getAttributeSet();
		Direction facing = attrs.facing;
		int baseWidth = ((Integer) attrs.size.getValue()).intValue();
		
		int axis = baseWidth / 2 + (negateOutput ? 10 : 0);
		int perp = 0;
		if (AppPreferences.GATE_SHAPE.get().equals(AppPreferences.SHAPE_RECTANGULAR)) {
			perp += 6;
		}
		Location loc = instance.getLocation();
		int cx;
		int cy;
		if (facing == Direction.NORTH) {
			cx = loc.getX() + perp;
			cy = loc.getY() + axis;
		} else if (facing == Direction.SOUTH) {
			cx = loc.getX() - perp;
			cy = loc.getY() - axis;
		} else if (facing == Direction.WEST) {
			cx = loc.getX() + axis;
			cy = loc.getY() - perp;
		} else {
			cx = loc.getX() - axis;
			cy = loc.getY() + perp;
		}
		instance.setTextField(StdAttr.LABEL, StdAttr.LABEL_FONT, cx, cy,
				TextField.H_CENTER, TextField.V_CENTER);
	}

	void computePorts(Instance instance) {
		GateAttributes attrs = (GateAttributes) instance.getAttributeSet();
		int inputs = attrs.inputs;

		Port[] ports = new Port[inputs + 1];
		ports[0] = new Port(0, 0, Port.OUTPUT, StdAttr.WIDTH);
		for (int i = 0; i < inputs; i++) {
			Location offs = getInputOffset(attrs, i);
			ports[i + 1] = new Port(offs.getX(), offs.getY(), Port.INPUT, StdAttr.WIDTH);
		}
		instance.setPorts(ports);
	}

	@Override
	public void propagate(InstanceState state) {
		GateAttributes attrs = (GateAttributes) state.getAttributeSet();
		int inputCount = attrs.inputs;
		int negated = attrs.negated;
		AttributeSet opts = state.getProject().getOptions().getAttributeSet();
		boolean errorIfUndefined = opts.getValue(Options.ATTR_GATE_UNDEFINED)
									.equals(Options.GATE_UNDEFINED_ERROR);

		Value[] inputs = new Value[inputCount];
		int numInputs = 0;
		boolean error = false;
		for (int i = 1; i <= inputCount; i++) {
			if (state.isPortConnected(i)) {
				int negatedBit = (negated >> (i - 1)) & 1;
				if (negatedBit == 1) {
					inputs[numInputs] = state.getPort(i).not();
				} else {
					inputs[numInputs] = state.getPort(i);
				}
				numInputs++;
			} else {
				if (errorIfUndefined) {
					error = true;
				}
			}
		}
		Value out = null;
		if (numInputs == 0 || error) {
			out = Value.createError(attrs.width);
		} else {
			out = computeOutput(inputs, numInputs, state);
			out = pullOutput(out, attrs.out);
		}
		state.setPort(0, out, GateAttributes.DELAY);
	}
	
	static Value pullOutput(Value value, Object outType) {
		if (outType == GateAttributes.OUTPUT_01) {
			return value;
		} else {
			Value[] v = value.getAll();
			if (outType == GateAttributes.OUTPUT_0Z) {
				for (int i = 0; i < v.length; i++) {
					if (v[i] == Value.TRUE) v[i] = Value.UNKNOWN;
				}
			} else if (outType == GateAttributes.OUTPUT_Z1) {
				for (int i = 0; i < v.length; i++) {
					if (v[i] == Value.FALSE) v[i] = Value.UNKNOWN;
				}
			}
			return Value.create(v);
		}
	}
	
	@Override
	protected Object getInstanceFeature(final Instance instance, Object key) {
		if (key == WireRepair.class) {
			return new WireRepair() {
				public boolean shouldRepairWire(WireRepairData data) {
					return AbstractGate.this.shouldRepairWire(instance, data);
				}
			};
		}
		if (key == ExpressionComputer.class) {
			return new ExpressionComputer() {
				public void computeExpression(Map<Location,Expression> expressionMap) {
					GateAttributes attrs = (GateAttributes) instance.getAttributeSet();
					int inputCount = attrs.inputs;
					int negated = attrs.negated;
	
					Expression[] inputs = new Expression[inputCount];
					int numInputs = 0;
					for (int i = 1; i <= inputCount; i++) {
						Expression e = expressionMap.get(instance.getPortLocation(i));
						if (e != null) {
							int negatedBit = (negated >> (i - 1)) & 1;
							if (negatedBit == 1) {
								e = Expressions.not(e);
							}
							inputs[numInputs] = e;
							++numInputs;
						}
					}
					if (numInputs > 0) {
						Expression out = AbstractGate.this.computeExpression(inputs, numInputs);
						expressionMap.put(instance.getPortLocation(0), out);
					}
				}
			};
		}
		return super.getInstanceFeature(instance, key);
	}
	
	Location getInputOffset(GateAttributes attrs, int index) {
		int inputs = attrs.inputs;
		Direction facing = attrs.facing;
		int size = ((Integer) attrs.size.getValue()).intValue();
		int axisLength = size + bonusWidth + (negateOutput ? 10 : 0);
		int negated = attrs.negated;

		int skipStart;
		int skipDist;
		int skipLowerEven = 10;
		if (inputs <= 3) {
			if (size < 40) {
				skipStart = -5;
				skipDist = 10;
				skipLowerEven = 10;
			} else if (size < 60 || inputs <= 2) {
				skipStart = -10;
				skipDist = 20;
				skipLowerEven = 20;
			} else {
				skipStart = -15;
				skipDist = 30;
				skipLowerEven = 30;
			}
		} else if (inputs == 4 && size >= 60) {
			skipStart = -5; 
			skipDist = 20;
			skipLowerEven = 0;
		} else {
			skipStart = -5;
			skipDist = 10;
			skipLowerEven = 10;
		}
		
		int dy;
		if ((inputs & 1) == 1) {
			dy = skipStart * (inputs - 1) + skipDist * index;
		} else {
			dy = skipStart * inputs + skipDist * index;
			if (index >= inputs / 2) dy += skipLowerEven;
		}

		int dx = axisLength;
		int negatedBit = (negated >> index) & 1;
		if (negatedBit == 1) {
			dx += 10;
		}
		
		if (facing == Direction.NORTH) {
			return Location.create(dy, dx);
		} else if (facing == Direction.SOUTH) {
			return Location.create(dy, -dx);
		} else if (facing == Direction.WEST) {
			return Location.create(dx, dy);
		} else {
			return Location.create(-dx, dy);
		}
	}
}
