/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: Sim.java
 * Asynchronous Logic Simulator engine
 * Original C Code written by Brent Serbin and Peter J. Gallant
 * Translated to Java by Steven M. Rubin, Sun Microsystems.
 *
 * Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved.
 *
 * Electric(tm) 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.
 *
 * Electric(tm) 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.sun.electric.tool.simulation.als;

import com.sun.electric.tool.simulation.DigitalSample;
import com.sun.electric.tool.simulation.MutableSignal;
import com.sun.electric.tool.simulation.Signal;
import com.sun.electric.tool.simulation.SimulationTool;
import com.sun.electric.tool.simulation.Stimuli;
import com.sun.electric.tool.simulation.als.ALS.Load;
import com.sun.electric.tool.simulation.als.ALS.Stat;
import com.sun.electric.tool.user.waveform.Panel;
import com.sun.electric.util.TextUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

/**
 * Class to do the engine of the ALS Simulator.
 */
public class Sim
{
	private ALS als;
	private List<Load> chekList = new ArrayList<Load>();
	private HashMap<ALS.Node,List<ALS.Trak>> tracking;

	private static String [] stateDesc = {"High", "Undefined", "Low"};
	private static String [] strengthDesc = {"Off-", "Weak-", "Weak-", "", "", "Strong-", "Strong-"};

	boolean tracing = false;

	Sim(ALS als)
	{
		this.als = als;
	}

	/**
	 * Method to initialize the simulator for a simulation run.  The
	 * vector link list is copied to a master event scheduling link list and
	 * the database is initialized to its starting values.  After these housekeeping
	 * tasks are completed the simulator is ready to start the actual simulation.
	 * Returns the time where the simulation quiesces.
	 */
	double initializeSimulator(boolean force)
	{
		als.timeAbs = 0.0;
		tracking = new HashMap<ALS.Node,List<ALS.Trak>>();

		while (als.linkFront != null)
		{
			if (als.linkFront.down != null)
			{
				als.linkFront.down.right = als.linkFront.right;
				als.linkFront = als.linkFront.down;
			} else
			{
				als.linkFront = als.linkFront.right;
			}
		}
		als.linkBack = null;

		for (ALS.Link linkHead = als.setRoot; linkHead != null; linkHead = linkHead.right)
		{
			ALS.Link linkPtr2 = new ALS.Link();
			if (linkPtr2 == null) return(als.timeAbs);
			linkPtr2.type = linkHead.type;
			linkPtr2.ptr = linkHead.ptr;
			linkPtr2.state = linkHead.state;
			linkPtr2.strength = linkHead.strength;
			linkPtr2.priority = linkHead.priority;
			linkPtr2.time = linkHead.time;
			linkPtr2.primHead = null;
			insertLinkList(linkPtr2);
		}

		for(ALS.Node nodeHead : als.nodeList)
		{
			nodeHead.sumState = Stimuli.LOGIC_LOW;
			nodeHead.sumStrength = Stimuli.OFF_STRENGTH;
			nodeHead.newState = new Integer(Stimuli.LOGIC_LOW);
			nodeHead.newStrength = Stimuli.OFF_STRENGTH;
			nodeHead.arrive = 0;
			nodeHead.depart = 0;
			nodeHead.tLast = 0.0;
			for(Stat statHead : nodeHead.statList)
			{
				statHead.newState = Stimuli.LOGIC_LOW;
				statHead.newStrength = Stimuli.OFF_STRENGTH;
				statHead.schedOp = 0;
			}
		}

		// now run the simulation
		boolean update = SimulationTool.isBuiltInResimulateEach();
		if (force) update = true;
		if (update)
		{
			// fire events until end of time or quiesced
			System.out.print("Simulating...");

			// determine highest time to simulate
            double tMax = 0;
            for(Signal<?> sig : als.sc.getSignals())
                tMax = Math.max(tMax, sig.getMaxTime());
			for(Iterator<Panel> it = als.ww.getPanels(); it.hasNext(); )
			{
				Panel wp = it.next();
				double panelMax = wp.getMaxXAxis();
				if (panelMax > tMax) tMax = panelMax;
			}

			while (als.linkFront != null && als.linkFront.time <= tMax)
			{
				if (fireEvent()) break;
				if (chekList.size() != 0)
				{
					if (scheduleNewEvents()) break;
				}
			}

			// redisplay results
			fillDisplayArrays();
			System.out.println("Done.  Ran to time " + TextUtils.convertToEngineeringNotation(als.timeAbs, "s"));
		}

		return als.timeAbs;
	}

	/**
	 * Method to extract the ALS simulation data and update the Stimuli database
	 */
	private void fillDisplayArrays()
	{
		for(ALS.Node node : tracking.keySet())
		{
			Signal<DigitalSample> sig = node.sig;
			List<ALS.Trak> trakHeads = tracking.get(node);
			for(ALS.Trak trakHead : trakHeads)
			{
				MutableSignal<DigitalSample> mSig = (MutableSignal<DigitalSample>)sig;
				DigitalSample ds = DigitalSample.fromOldStyle(trakHead.state&Stimuli.LOGIC);
                if (mSig.getSample(trakHead.time) == null)
                {
                	mSig.addSample(trakHead.time, ds);
                } else
                {
                	mSig.replaceSample(trakHead.time, ds);
                }
            }
		}
		
		als.ww.repaint();
	}

	/**
	 * Method to get the entry from the front of the event scheduling
	 * link list and updates the database accordingly.  If a node is updated by a
	 * user defined vector the node value is changed as specified in the linked-list
	 * entry.  If a transition fired, all the output nodes are updated as specified
	 * in the truth table for the transition.  Returns true on error.
	 */
	private boolean fireEvent()
	{
		als.timeAbs = als.linkFront.time;
		ALS.Link linkHead = als.linkFront;
		if (als.linkFront.down != null)
		{
			als.linkFront = als.linkFront.down;
			als.linkFront.left = null;
			als.linkFront.right = linkHead.right;
			als.linkFront.up = linkHead.up;
			if (als.linkFront.right != null)
			{
				als.linkFront.right.left = als.linkFront;
			} else
			{
				als.linkBack = als.linkFront;
			}
		} else
		{
			als.linkFront = als.linkFront.right;
			if (als.linkFront != null)
			{
				als.linkFront.left = null;
			} else
			{
				als.linkBack = null;
			}
		}

		tracing = false;
		switch (linkHead.type)
		{
			case 'G':
				ALS.Stat statHead = (ALS.Stat)linkHead.ptr;
				if (statHead.nodePtr.traceNode)
				{
					String s2 = als.computeNodeName(statHead.nodePtr);
					System.out.println(TextUtils.convertToEngineeringNotation(als.timeAbs) +
						": Firing gate " + statHead.primPtr.name + statHead.primPtr.level + ", net " + s2);
					tracing = true;
				}
				if (statHead.schedState != linkHead.state ||
					statHead.schedOp != linkHead.operatr ||
					statHead.schedStrength != linkHead.strength)
				{
					break;
				}
				statHead.schedOp = 0;

				char operatr = linkHead.operatr;
				int operand = 0;
				if (operatr < 128)
				{
					operand = ((Integer)linkHead.state).intValue();
				} else
				{
					operatr -= 128;
//					ALS.Node nodeHead = (ALS.Node)linkHead.state;
//					operand = nodeHead.sumState;
				}

				int state = 0;
				switch (operatr)
				{
					case '=':
						state = operand;
						break;
					case '+':
						state = statHead.nodePtr.sumState + operand;
						break;
					case '-':
						state = statHead.nodePtr.sumState - operand;
						break;
					case '*':
						state = statHead.nodePtr.sumState * operand;
						break;
					case '/':
						state = statHead.nodePtr.sumState / operand;
						break;
					case '%':
						state = statHead.nodePtr.sumState % operand;
						break;
					default:
						System.out.println("Invalid arithmetic operator: " + operatr);
						return true;
				}

				if (state == statHead.newState &&
					linkHead.strength == statHead.newStrength)
				{
					break;
				}
				statHead.newState = state;
				statHead.newStrength = linkHead.strength;
				createCheckList(statHead.nodePtr, linkHead);
				break;

			case 'N':
				ALS.Node nodeHead = (ALS.Node)linkHead.ptr;
				if (nodeHead.traceNode)
				{
					String s2 = als.computeNodeName(nodeHead);
					System.out.println(TextUtils.convertToEngineeringNotation(als.timeAbs) + ": Changed state of net " + s2);
					tracing = true;
				}
				if (linkHead.state == nodeHead.newState &&
					linkHead.strength == nodeHead.newStrength)
						break;

				nodeHead.newState = linkHead.state;
				nodeHead.newStrength = linkHead.strength;
				createCheckList(nodeHead, linkHead);
				break;

			case 'C':
				double time = als.timeAbs;
				ALS.Row rowHead = (ALS.Row)linkHead.ptr;
				for(Object obj : rowHead.inList)
				{
					ALS.Link vectHead = (ALS.Link)obj;
					ALS.Link linkPtr2 = new ALS.Link();
					linkPtr2.type = 'N';
					linkPtr2.ptr = vectHead.ptr;
					linkPtr2.state = vectHead.state;
					linkPtr2.strength = vectHead.strength;
					linkPtr2.priority = vectHead.priority;
					linkPtr2.time = time;
					linkPtr2.primHead = null;
					insertLinkList(linkPtr2);
					time += vectHead.time;
				}
				if (((Integer)linkHead.state).intValue() == 0)
				{
					calculateClockTime(linkHead, rowHead);
					return false;
				}
				linkHead.state = new Integer(((Integer)linkHead.state).intValue() - 1);
				if (((Integer)linkHead.state).intValue() != 0)
				{
					calculateClockTime(linkHead, rowHead);
					return false;
				}
		}

		return false;
	}

	/**
	 * Method to calculate the sum state and strength for a node and if
	 * it has changed from a previous check it will enter the input transition list
	 * into a master check list that is used by the event scheduling routine.
	 * It should be noted that it is necessary to calculate the sum strength and
	 * state for a node because it is possible to have nodes that have more than
	 * one transition driving it.
	 */
	private void createCheckList(ALS.Node nodeHead, ALS.Link linkHead)
	{
		// get initial state of the node
		int state = ((Integer)nodeHead.newState).intValue();
		int strength = nodeHead.newStrength;

		// print state of signal if this signal is being traced
		if (tracing)
		{
			System.out.println("  Formerly " + strengthDesc[nodeHead.sumStrength] + stateDesc[nodeHead.sumState+3] +
				", starts at " + strengthDesc[strength] + stateDesc[state+3]);
		}

		// look at all factors affecting the node
		for(Stat statHead : nodeHead.statList)
		{
			int thisState = statHead.newState;
			int thisStrength = statHead.newStrength;
			if (tracing)
				System.out.println("    " + strengthDesc[thisStrength] + stateDesc[thisState+3] +
					" from " + statHead.primPtr.name + statHead.primPtr.level);

			// higher strength overrides previous node state
			if (thisStrength > strength)
			{
				state = thisState;
				strength = thisStrength;
				continue;
			}

			// same strength: must arbitrate
			if (thisStrength == strength)
			{
				if (thisState != state)
					state = Stimuli.LOGIC_X;
			}
		}

		// if the node has nothing driving it, set it to the old value
		if (strength == Stimuli.OFF_STRENGTH)
		{
			state = nodeHead.sumState;
			strength = Stimuli.NODE_STRENGTH;
		}

		// stop now if node state did not change
		if (nodeHead.sumState == state && nodeHead.sumStrength == strength)
		{
			if (tracing) System.out.println("    NO CHANGE");
			return;
		}

		if (nodeHead.sig != null)
		{
			List<ALS.Trak> nodeData = tracking.get(nodeHead);
			if (nodeData == null)
			{
				nodeData = new ArrayList<ALS.Trak>();
				tracking.put(nodeHead, nodeData);
			}

			ALS.Trak trakHead = new ALS.Trak();
			trakHead.state = state | strength;
			trakHead.time = als.timeAbs;
			nodeData.add(trakHead);
		}
		if (tracing)
			System.out.println("    BECOMES " + strengthDesc[strength] + stateDesc[state+3]);

		nodeHead.sumState = state;
		nodeHead.sumStrength = strength;
		nodeHead.tLast = als.timeAbs;

		als.driveNode = nodeHead;
		for(Load l : nodeHead.pinList)
			chekList.add(l);
	}

	/**
	 * Method to examine the truth tables for the transitions that are
	 * specified in the checking list.  If there is a match between a truth table
	 * entry and the state of the logic network the transition is scheduled
	 * for firing.  Returns true on error.
	 */
	private boolean scheduleNewEvents()
	{
		// make a copy of the event list, and clear the main list
		List<Load> chekListCopy =  new ArrayList<Load>();
		for(Load l : chekList)
			chekListCopy.add(l);
		chekList.clear();

		for(Load chekHead : chekListCopy)
		{
			ALS.Model primHead = (ALS.Model)chekHead.ptr;
			if (primHead.type == 'F')
			{
				ALS.Func funcHead = (ALS.Func)primHead.ptr;
				funcHead.procPtr.simulate(primHead);
				continue;
			}

			for (ALS.Row rowHead = (ALS.Row)primHead.ptr; rowHead != null; rowHead = rowHead.next)
			{
				int flag = 1;
				for(Object obj : rowHead.inList)
				{
					ALS.IO ioHead = (ALS.IO)obj;
					int operatr = ioHead.operatr;
					int operand;
					if (operatr < 128)
					{
						operand = ((Integer)ioHead.operand).intValue();
					} else
					{
						operatr -= 128;
						ALS.Node nodeHead = (ALS.Node)ioHead.operand;
						operand = nodeHead.sumState;
					}

					switch (operatr)
					{
						case '=':
							if (((ALS.Node)ioHead.nodePtr).sumState != operand) flag = 0;
							break;
						case '!':
							if (((ALS.Node)ioHead.nodePtr).sumState == operand) flag = 0;
							break;
						case '<':
							if (((ALS.Node)ioHead.nodePtr).sumState >= operand) flag = 0;
							break;
						case '>':
							if (((ALS.Node)ioHead.nodePtr).sumState <= operand) flag = 0;
							break;
						default:
							System.out.println("Invalid logical operator: " + operatr);
							return true;
					}

					if (flag == 0) break;
				}

				if (flag != 0)
				{
					calculateEventTime(primHead, rowHead);
					break;
				}
			}
		}
		return false;
	}

	/**
	 * Method to calculate the time when the next occurrence of a set of
	 * clock vectors is to be added to the event scheduling linked-list.
	 *
	 * Calling Arguments:
	 *	linkHead = pointer to the link element to be reinserted into the list
	 *  rowHead  = pointer to a row element containing timing information
	 */
	private void calculateClockTime(ALS.Link linkHead, ALS.Row rowHead)
	{
		double time = als.timeAbs;

		if (rowHead.delta != 0) time += rowHead.delta;
		if (rowHead.linear != 0)
		{
			double prob = Math.random();
			time += 2.0 * prob * rowHead.linear;
		}

		/*
		 * if (rowHead.exp)
		 * {
		 * 	prob = rand() / MAXINTBIG;
		 * 	time += (-log(prob) * (rowHead.exp));
		 * }
		 */

		linkHead.time = time;
		insertLinkList(linkHead);
	}

	/**
	 * Method to calculate the time of occurrence of an event and then
	 * places an entry into the event scheduling linked-list for later execution.
	 * Returns true on error.
	 *
	 * Calling Arguments:
	 *	primHead  = pointer to the primitive to be scheduled for firing
	 *	rowHead  = pointer to the row containing the event to be scheduled
	 */
	private void calculateEventTime(ALS.Model primHead, ALS.Row rowHead)
	{
		double time = 0.0;
		int priority = primHead.priority;

		if (rowHead.delta != 0) time += rowHead.delta;
		if (rowHead.abs != 0) time += rowHead.abs;
		if (rowHead.linear != 0)
		{
			double prob = Math.random();
			time += 2.0 * prob * rowHead.linear;
		}

		/*
		 * if (rowHead.exp)
		 * {
		 * 	prob = rand() / MAXINTBIG;
		 * 	time += (-log(prob) * (rowHead.exp));
		 * }
		 */

		if (rowHead.random != 0)
		{
			double prob = Math.random();
			if (prob <= rowHead.random)
			{
				priority = -1;
			}
		}

		if (primHead.fanOut != 0)
		{
			Iterator<Object> it = rowHead.outList.iterator();
			ALS.IO ioPtr = (ALS.IO)it.next();
			ALS.Stat statHead = (ALS.Stat)ioPtr.nodePtr;
			time *= statHead.nodePtr.load;
		}
		time += als.timeAbs;

		for(Object obj : rowHead.outList)
		{
			ALS.IO ioHead = (ALS.IO)obj;
			ALS.Stat statHead = (ALS.Stat)ioHead.nodePtr;
			if (statHead.schedOp == ioHead.operatr &&
				statHead.schedState.equals(ioHead.operand) &&
				statHead.schedStrength == ioHead.strength)
			{
				continue;
			}

			ALS.Link linkPtr2 = new ALS.Link();
			linkPtr2.type = 'G';
			linkPtr2.ptr = statHead;
			linkPtr2.operatr = statHead.schedOp = ioHead.operatr;
			linkPtr2.state = statHead.schedState = ioHead.operand;
			linkPtr2.strength = statHead.schedStrength = ioHead.strength;
			linkPtr2.time = time;
			linkPtr2.priority = priority;
			linkPtr2.primHead = primHead;
			if (tracing)
			{
				System.out.println("      Schedule(G): " + statHead.primPtr.name + statHead.primPtr.level +
					" at " + TextUtils.convertToEngineeringNotation(time));
			}
			insertLinkList(linkPtr2);
		}
	}

	/**
	 * Method to insert a data element into a linked-list that is 2
	 * dimensionally sorted first by time and then priority.  This link list is
	 * used to schedule events for the simulation.
	 *
	 * Calling Arguments:
	 *	linkHead = pointer to the data element that is going to be inserted
	 */
	void insertLinkList(ALS.Link linkHead)
	{
		// linkPtr1Is: 0: ALS.linkBack  1: linkPtr2.up  2: linkPtr2.left
		int linkPtr1Is = 0;
		ALS.Link linkPtr2 = als.linkBack;
		ALS.Link linkPtr2Val = linkPtr2;
		ALS.Link linkPtr3 = null;
		for(;;)
		{
			if (linkPtr2 == null)
			{
				als.linkFront = linkHead;
				switch (linkPtr1Is)
				{
					case 0: als.linkBack = linkHead;      break;
					case 1: linkPtr2Val.up = linkHead;    break;
					case 2: linkPtr2Val.left = linkHead;  break;
				}
				linkHead.left = null;
				linkHead.right = linkPtr3;
				linkHead.up = linkHead;
				linkHead.down = null;
				return;
			}

			if (linkPtr2.time < linkHead.time)
			{
				linkPtr2.right = linkHead;
				switch (linkPtr1Is)
				{
					case 0: als.linkBack = linkHead;      break;
					case 1: linkPtr2Val.up = linkHead;    break;
					case 2: linkPtr2Val.left = linkHead;  break;
				}
				linkHead.left = linkPtr2;
				linkHead.right = linkPtr3;
				linkHead.up = linkHead;
				linkHead.down = null;
				return;
			}

			if (linkPtr2.time == linkHead.time)
			{
				if (linkPtr2.priority > linkHead.priority)
				{
					linkHead.left = linkPtr2.left;
					linkHead.right = linkPtr2.right;
					linkHead.down = linkPtr2;
					linkHead.up = linkPtr2.up;
					linkPtr2.up = linkHead;
					switch (linkPtr1Is)
					{
						case 0: als.linkBack = linkHead;      break;
						case 1: linkPtr2Val.up = linkHead;    break;
						case 2: linkPtr2Val.left = linkHead;  break;
					}
					if (linkHead.left != null)
					{
						linkHead.left.right = linkHead;
					} else
					{
						als.linkFront = linkHead;
					}
					return;
				}

				linkPtr1Is = 1;
				linkPtr2Val = linkPtr2;
				linkPtr2 = linkPtr2.up;
				linkPtr3 = null;
				for(;;)
				{
					if (linkPtr2.priority <= linkHead.priority)
					{
						linkPtr2.down = linkHead;
						switch (linkPtr1Is)
						{
							case 0: als.linkBack = linkHead;      break;
							case 1: linkPtr2Val.up = linkHead;    break;
							case 2: linkPtr2Val.left = linkHead;  break;
						}
						linkHead.up = linkPtr2;
						linkHead.down = linkPtr3;
						return;
					}

					linkPtr3 = linkPtr2;
					linkPtr1Is = 1;
					linkPtr2Val = linkPtr2;
					linkPtr2 = linkPtr2.up;
				}
			}

			linkPtr3 = linkPtr2;
			linkPtr1Is = 2;
			linkPtr2Val = linkPtr2;
			linkPtr2 = linkPtr2.left;
		}
	}
}
