/* 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.circuit;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.cburch.logisim.circuit.Strings;
import com.cburch.logisim.data.AbstractAttributeSet;
import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.AttributeOption;
import com.cburch.logisim.data.Attributes;
import com.cburch.logisim.data.BitWidth;
import com.cburch.logisim.data.Direction;
import com.cburch.logisim.instance.StdAttr;

class SplitterAttributes extends AbstractAttributeSet {
	public static final AttributeOption APPEAR_LEGACY
		= new AttributeOption("legacy", Strings.getter("splitterAppearanceLegacy"));
	public static final AttributeOption APPEAR_LEFT
		= new AttributeOption("left", Strings.getter("splitterAppearanceLeft"));
	public static final AttributeOption APPEAR_RIGHT
		= new AttributeOption("right", Strings.getter("splitterAppearanceRight"));
	public static final AttributeOption APPEAR_CENTER
		= new AttributeOption("center", Strings.getter("splitterAppearanceCenter"));
	
	public static final Attribute<AttributeOption> ATTR_APPEARANCE
		= Attributes.forOption("appear", Strings.getter("splitterAppearanceAttr"),
				new AttributeOption[] { APPEAR_LEFT, APPEAR_RIGHT, APPEAR_CENTER,
					APPEAR_LEGACY});
	
	public static final Attribute<BitWidth> ATTR_WIDTH
		= Attributes.forBitWidth("incoming", Strings.getter("splitterBitWidthAttr"));
	public static final Attribute<Integer> ATTR_FANOUT
		= Attributes.forIntegerRange("fanout", Strings.getter("splitterFanOutAttr"), 1, 32);

	private static final List<Attribute<?>> INIT_ATTRIBUTES
		= Arrays.asList(new Attribute<?>[] {
			StdAttr.FACING, ATTR_FANOUT, ATTR_WIDTH, ATTR_APPEARANCE,
		});

	private static final String unchosen_val = "none";

	private static class BitOutOption {
		int value;
		boolean isVertical;
		boolean isLast;

		BitOutOption(int value, boolean isVertical, boolean isLast) {
			this.value = value;
			this.isVertical = isVertical;
			this.isLast = isLast;
		}

		@Override
		public String toString() {
			if (value < 0) {
				return Strings.get("splitterBitNone");
			} else {
				String ret = "" + value;
				Direction noteDir;
				if (value == 0) {
					noteDir = isVertical ? Direction.NORTH : Direction.EAST;
				} else if (isLast) {
					noteDir = isVertical ? Direction.SOUTH : Direction.WEST;
				} else {
					noteDir = null;
				}
				if (noteDir != null) {
					ret += " (" + noteDir.toVerticalDisplayString() + ")";
				}
				return ret;
			}
		}
	}

	static class BitOutAttribute extends Attribute<Integer> {
		int which;
		BitOutOption[] options;

		private BitOutAttribute(int which, BitOutOption[] options) {
			super("bit" + which, Strings.getter("splitterBitAttr", "" + which));
			this.which = which;
			this.options = options;
		}
		
		private BitOutAttribute createCopy() {
			return new BitOutAttribute(which, options);
		}
		
		public Object getDefault() {
			return Integer.valueOf(which + 1);
		}

		@Override
		public Integer parse(String value) {
			if (value.equals(unchosen_val)) {
				return Integer.valueOf(0);
			} else {
				return Integer.valueOf(1 + Integer.parseInt(value));
			}
		}

		@Override
		public String toDisplayString(Integer value) {
			int index = value.intValue();
			return options[index].toString();
		}

		@Override
		public String toStandardString(Integer value) {
			int index = value.intValue();
			if (index == 0) {
				return unchosen_val;
			} else {
				return "" + (index - 1);
			}
		}

		@Override
		public java.awt.Component getCellEditor(Integer value) {
			int index = value.intValue();
			javax.swing.JComboBox combo = new javax.swing.JComboBox(options);
			combo.setSelectedIndex(index);
			return combo;
		}
	}

	private ArrayList<Attribute<?>> attrs = new ArrayList<Attribute<?>>(INIT_ATTRIBUTES);
	private SplitterParameters parameters;
	AttributeOption appear = APPEAR_LEFT;
	Direction facing = Direction.EAST;
	byte fanout = 2;                 // number of ends this splits into
	byte[] bit_end = new byte[2];    // how each bit maps to an end (0 if nowhere);
									 //   other values will be between 1 and fanout
	BitOutOption[] options = null;

	SplitterAttributes() {
		configureOptions();
		configureDefaults();
		parameters = new SplitterParameters(this);
	}
	
	Attribute<?> getBitOutAttribute(int index) {
		return attrs.get(INIT_ATTRIBUTES.size() + index);
	}

	@Override
	protected void copyInto(AbstractAttributeSet destObj) {
		SplitterAttributes dest = (SplitterAttributes) destObj;
		dest.parameters = this.parameters;
		dest.attrs = new ArrayList<Attribute<?>>(this.attrs.size());
		dest.attrs.addAll(INIT_ATTRIBUTES);
		for (int i = INIT_ATTRIBUTES.size(), n = this.attrs.size(); i < n; i++) {
			BitOutAttribute attr = (BitOutAttribute) this.attrs.get(i);
			dest.attrs.add(attr.createCopy());
		}

		dest.facing = this.facing;
		dest.fanout = this.fanout;
		dest.appear = this.appear;
		dest.bit_end = this.bit_end.clone();
		dest.options = this.options;
	}
	
	public SplitterParameters getParameters() {
		SplitterParameters ret = parameters;
		if (ret == null) {
			ret = new SplitterParameters(this);
			parameters = ret;
		}
		return ret;
	}

	@Override
	public List<Attribute<?>> getAttributes() {
		return attrs;
	}

	@Override
	@SuppressWarnings("unchecked")
	public <V> V getValue(Attribute<V> attr) {
		if (attr == StdAttr.FACING) {
			return (V) facing;
		} else if (attr == ATTR_FANOUT) {
			return (V) Integer.valueOf(fanout);
		} else if (attr == ATTR_WIDTH) {
			return (V) BitWidth.create(bit_end.length);
		} else if (attr == ATTR_APPEARANCE) {
			return (V) appear;
		} else if (attr instanceof BitOutAttribute) {
			BitOutAttribute bitOut = (BitOutAttribute) attr;
			return (V) Integer.valueOf(bit_end[bitOut.which]);
		} else {
			return null;
		}
	}

	@Override
	public <V> void setValue(Attribute<V> attr, V value) {
		if (attr == StdAttr.FACING) {
			facing = (Direction) value;
			configureOptions();
			parameters = null;
		} else if (attr == ATTR_FANOUT) {
			int newValue = ((Integer) value).intValue();
			byte[] bits = bit_end;
			for (int i = 0; i < bits.length; i++) {
				if (bits[i] >= newValue) bits[i] = (byte) (newValue - 1);
			}
			fanout = (byte) newValue;
			configureOptions();
			configureDefaults();
			parameters = null;
		} else if (attr == ATTR_WIDTH) {
			BitWidth width = (BitWidth) value;
			bit_end = new byte[width.getWidth()];
			configureOptions();
			configureDefaults();
		} else if (attr == ATTR_APPEARANCE) {
			appear = (AttributeOption) value;
			parameters = null;
		} else if (attr instanceof BitOutAttribute) {
			BitOutAttribute bitOutAttr = (BitOutAttribute) attr;
			int val;
			if (value instanceof Integer) {
				val = ((Integer) value).intValue();
			} else {
				val= ((BitOutOption) value).value + 1;
			}
			if (val >= 0 && val <= fanout) {
				bit_end[bitOutAttr.which] = (byte) val;
			}
		} else {
			throw new IllegalArgumentException("unknown attribute " + attr);
		}
		fireAttributeValueChanged(attr, value);
	}

	private void configureOptions() {
		// compute the set of options for BitOutAttributes
		options = new BitOutOption[fanout + 1];
		boolean isVertical = facing == Direction.EAST || facing == Direction.WEST;
		for (int i = -1; i < fanout; i++) {
			options[i + 1] = new BitOutOption(i, isVertical, i == fanout - 1);
		}

		// go ahead and set the options for the existing attributes
		int offs = INIT_ATTRIBUTES.size();
		int curNum = attrs.size() - offs;
		for (int i = 0; i < curNum; i++) {
			BitOutAttribute attr = (BitOutAttribute) attrs.get(offs + i);
			attr.options = options;
		}
	}
	
	private void configureDefaults() {
		int offs = INIT_ATTRIBUTES.size();
		int curNum = attrs.size() - offs;

		// compute default values
		byte[] dflt = computeDistribution(fanout, bit_end.length, 1);

		boolean changed = curNum != bit_end.length;
		
		// remove excess attributes
		while (curNum > bit_end.length) {
			curNum--;
			attrs.remove(offs + curNum);
		}

		// set existing attributes
		for (int i = 0; i < curNum; i++) {
			if (bit_end[i] != dflt[i]) {
				BitOutAttribute attr = (BitOutAttribute) attrs.get(offs + i);
				bit_end[i] = dflt[i];
				fireAttributeValueChanged(attr, Integer.valueOf(bit_end[i]));
			}
		}

		// add new attributes
		for (int i = curNum; i < bit_end.length; i++) {
			BitOutAttribute attr = new BitOutAttribute(i, options);
			bit_end[i] = dflt[i];
			attrs.add(attr);
		}
		
		if (changed) fireAttributeListChanged();
	}
	
	static byte[] computeDistribution(int fanout, int bits, int order) {
		byte[] ret = new byte[bits];
		if (order >= 0) {
			if (fanout >= bits) {
				for (int i = 0; i < bits; i++) ret[i] = (byte) (i + 1);
			} else {
				int threads_per_end = bits / fanout;
				int ends_with_extra = bits % fanout;
				int cur_end = -1; // immediately increments
				int left_in_end = 0;
				for (int i = 0; i < bits; i++) {
					if (left_in_end == 0) {
						++cur_end;
						left_in_end = threads_per_end;
						if (ends_with_extra > 0) {
							++left_in_end;
							--ends_with_extra;
						}
					}
					ret[i] = (byte) (1 + cur_end);
					--left_in_end;
				}
			}
		} else {
			if (fanout >= bits) {
				for (int i = 0; i < bits; i++) ret[i] = (byte) (fanout - i);
			} else {
				int threads_per_end = bits / fanout;
				int ends_with_extra = bits % fanout;
				int cur_end = -1;
				int left_in_end = 0;
				for (int i = bits - 1; i >= 0; i--) {
					if (left_in_end == 0) {
						++cur_end;
						left_in_end = threads_per_end;
						if (ends_with_extra > 0) {
							++left_in_end;
							--ends_with_extra;
						}
					}
					ret[i] = (byte) (1 + cur_end);
					--left_in_end;
				}
			}
		}
		return ret;
	}
}
