text: /**
   * @fileoverview
   * Utils method about chem objects.
   * @author Partridge Jiang
   */
  
  /*
   * requires /utils/kekule.utils.js
   * requires /core/kekule.common.js
   * requires /core/kekule.valences.js
   * requires /core/kekule.structures.js
   */
  
  (function () {
  
  "use strict";
  
  var AU = Kekule.ArrayUtils;
  
  /**
   * Util class to manipulate ctab based chem structures.
   * @class
   */
  Kekule.ChemStructureUtils = {
  	/**
  	 * Returns median of all input connector lengths.
  	 * @param {Array} connectors
  	 * @param {Int} coordMode
  	 * @param {Bool} allowCoordBorrow
  	 * @return {Float}
  	 */
  	getConnectorLengthMedian: function(connectors, coordMode, allowCoordBorrow)
  	{
  		var lengths = [];
  		for (var i = 0, l = connectors.length; i < l; ++i)
  		{
  			var connector = connectors[i];
  			if (connector && connector.getLength)
  			{
  				var length = connector.getLength(coordMode, allowCoordBorrow);
  				if (length)
  					lengths.push(length);
  			}
  		}
  		if (l === 0)  // no connectors at all
  			return 1;  // TODO: this value should be calculated
  		if (l <= 1)
  			return lengths[0];
  		else
  		{
  			// sort lengths to find the median one
  			lengths.sort();
  			var count = lengths.length;
  			var result = (count % 2)? lengths[(count + 1) >> 1]: (lengths[count >> 1] + lengths[(count >> 1) - 1]) / 2;
  			return result;
  		}
  	},
  	/**
  	 * Returns structured children of chemObj. The type of chemObj can be:
  	 *   {@link Kekule.ChemObjList}: returns chemObj.getItems();
  	 *   {@link Kekule.ChemStructureObjectGroup}: returns chemObj.getAllObjs();
  	 *   {@link Kekule.CompositeMolecule}: returns chemObj.getSubMolecules().getAllObjs().
  	 *   {@link Kekule.ChemSpaceElement} or {@link Kekule.ChemSpace}: returns all child structured objects inside it.
  	 * Other types will simply return [chemObj].
  	 * If param cascade is true, each childObj will also be checked.
  	 * @param {Variant} chemObj
  	 * @param {Bool} cascade
  	 * @returns {Array}
  	 */
  	getChildStructureObjs: function(chemObj, cascade)
  	{
  		var isComplex = true;
  		var result;
  		if (chemObj instanceof Kekule.CompositeMolecule)
  			result = chemObj.getSubMolecules().getAllObjs();
  		else if (chemObj instanceof Kekule.ChemStructureObjectGroup)
  			result = chemObj.getAllObjs();
  		else if (chemObj instanceof Kekule.ChemObjList)
  			result = chemObj.getItems();
  		else if (chemObj instanceof Kekule.ChemSpaceElement)
  			result = chemObj.getChildren().getItems();
  		else if (chemObj instanceof Kekule.ChemSpace)
  			result = chemObj.getChildren();
  		else
  		{
  			isComplex = false;
  			return [chemObj];
  		}
  		result = [].concat(result);  // clone result, avoid affect properties of chemObj
  
  		// if not returned and cascade, need future check
  		if (cascade && isComplex)
  		{
  			var newResult = [];
  			for (var i = 0, l = result.length; i < l; ++i)
  			{
  				var obj = result[i];
  				var cascadeChilds = Kekule.ChemStructureUtils.getChildStructureObjs(obj, cascade);
  				if (!cascadeChilds.length || (cascadeChilds.length === 1 && cascadeChilds[0] === obj))  // can not find cascade children
  					Kekule.ArrayUtils.pushUnique(newResult, obj);
  				else  // children find, use them to replace obj
  				{
  					Kekule.ArrayUtils.pushUnique(newResult, cascadeChilds);
  				}
  			}
  			result = newResult;
  		}
  		//console.log(result);
  		return result;
  	},
  
  	/**
  	 * Returns all child structure fragments among children of chemObj.
  	 * @param {Variant} chemObj
  	 * @param {Bool} cascade
  	 * @returns {Array}
  	 */
  	getAllStructFragments: function(chemObj, cascade)
  	{
  		if (chemObj instanceof Kekule.StructureFragment)
  			return [chemObj];
  
  		var childObjs = Kekule.ChemStructureUtils.getChildStructureObjs(chemObj, cascade);
  		var result = [];
  		for (var i = 0, l = childObjs.length; i < l; ++i)
  		{
  			if (childObjs[i] instanceof Kekule.StructureFragment)
  				Kekule.ArrayUtils.pushUnique(result, childObjs[i]);
  		}
  		return result;
  	},
  
  	/**
  	 * Find all child structure fragments among children of chemObj, then merge them into one.
  	 * @param {Variant} chemObj
  	 * @param {Class} newFragmentClass If set, new fragment will be based on this class.
  	 *   Otherwise an instance of {@link Kekule.Molecule} will be created.
  	 * @return {Kekule.StructureFragment}
  	 */
  	getTotalStructFragment: function(chemObj, newFragmentClass)
  	{
  		var fragments = Kekule.ChemStructureUtils.getAllStructFragments(chemObj, true);
  		var count = fragments.length;
  		if (count <= 0)  // nothing found
  			return null;
  		else if (count === 1)  // only one, returns it directly
  			return fragments[0];
  		else  // need merge
  		{
  			return Kekule.ChemStructureUtils.mergeStructFragments(fragments, newFragmentClass);
  		}
  	},
  
  	/**
  	 * Returns nodes or connectors that should be removed cascadely with chemStructObj.
  	 * @param {Object} chemStructObj
  	 * @returns {Array}
  	 * @deprecated
  	 */
  	getCascadeDeleteObjs: function(chemStructObj)
  	{
  		var result = [];
  		// all usual connectors (two ends) connected to chemStructObj should be removed
  		var linkedConnectors = chemStructObj.getLinkedConnectors? chemStructObj.getLinkedConnectors(): [];
  		for (var i = 0, l = linkedConnectors.length; i < l; ++i)
  		{
  			var connector = linkedConnectors[i];
  			if (connector.getConnectedObjs().length <= 2)
  			{
  				Kekule.ArrayUtils.pushUnique(result, connector);
  				var newCascadeObjs = Kekule.ChemStructureUtils.getCascadeDeleteObjs(connector);
  				Kekule.ArrayUtils.pushUnique(result, newCascadeObjs);
  			}
  		}
  
  		if (chemStructObj instanceof Kekule.ChemStructureNode)
  		{
  			// no additional objects should be delete
  		}
  		else if (chemStructObj instanceof Kekule.ChemStructureConnector)
  		{
  			// nodes connected with and only with this connector should be removed
  			var objs = chemStructObj.getConnectedObjs();
  			for (var i = 0, l = objs.length; i < l; ++i)
  			{
  				var obj = objs[i];
  				if (obj instanceof Kekule.ChemStructureNode)
  				{
  					if (obj.getLinkedConnectors().length <= 1)
  						Kekule.ArrayUtils.pushUnique(result, obj);
  				}
  			}
  		}
  		else  // other objects
  			;
  
  		return result;
  	},
  
  	/**
  	 * Move nodes and connectors from target to dest structure fragment.
  	 * @param {Kekule.StructureFragment} target
  	 * @param {Kekule.StructureFragment} dest
  	 * @param {Array} moveNodes
  	 * @param {Array} moveConnectors
  	 * @param {Bool} ignoreAnchorNodes
  	 */
  	moveChildBetweenStructFragment: function(target, dest, moveNodes, moveConnectors, ignoreAnchorNodes)
  	{
  		/*
  		var CU = Kekule.CoordUtils;
  
  		target.beginUpdate();
  		dest.beginUpdate();
  		var anchorNodes = target.getAnchorNodes();
  		try
  		{
  			// TODO: here we need change coord if essential
  			var targetCoord2D = target.getAbsCoord2D();
  			var targetCoord3D = target.getAbsCoord3D();
  			var destCoord2D = dest.getAbsCoord2D();
  			var destCoord3D = dest.getAbsCoord3D();
  
  			var coordDelta2D = CU.substract(targetCoord2D, destCoord2D);
  			var coordDelta3D = CU.substract(targetCoord3D, destCoord3D);
  
  			//console.log('coordDelta', coordDelta2D, coordDelta3D);
  
  			var nodes = Kekule.ArrayUtils.clone(moveNodes);
  			var connectors = Kekule.ArrayUtils.clone(moveConnectors);
  			for (var i = 0, l = nodes.length; i < l; ++i)
  			{
  				var node = nodes[i];
  				var index = target.indexOfNode(node);
  				if (index >= 0)
  				{
  					target.removeNodeAt(index, true);  // preserve linked connectors
  
  					var oldCoord2D = node.getCoord2D();
  					if (oldCoord2D)
  					{
  						var newCoord2D = CU.add(oldCoord2D, coordDelta2D);
  						node.setCoord2D(newCoord2D);
  					}
  					var oldCoord3D = node.getCoord3D();
  					if (oldCoord3D)
  					{
  						var newCoord3D = CU.add(oldCoord3D, coordDelta3D);
  						node.setCoord2D(newCoord3D);
  					}
  
  					dest.appendNode(node);
  					if (anchorNodes.indexOf(node)>= 0)
  					{
  						target.removeAnchorNode(node);
  						if (!ignoreAnchorNodes)
  							dest.appendAnchorNode(node);
  					}
  				}
  			}
  			for (var i = 0, l = connectors.length; i < l; ++i)
  			{
  				var connector = connectors[i];
  				var index = target.indexOfConnector(connector);
  				if (index >= 0)
  				{
  					target.removeConnectorAt(index, true);  // preserve linked objects
  					dest.appendConnector(connector);
  				}
  			}
  		}
  		finally
  		{
  			//console.log('[struct merge done]');
  			dest.endUpdate();
  			target.endUpdate();
  		}
  		*/
  		return Kekule.StructureFragment.moveChildBetweenStructFragment(target, dest, moveNodes, moveConnectors, ignoreAnchorNodes);
  	},
  
  	/** @private */
  	_getCascadeConnectedNodesAndConnectors: function(connector, parentStructFragment)
  	{
  		var connectors = [];
  		var nodes = [];
  		var objs = connector.getConnectedObjs();
  		for (var j = 0, k = objs.length; j < k; ++j)
  		{
  			var obj = objs[j];
  			if (obj !== connector)
  			{
  				if (parentStructFragment)
  					obj = parentStructFragment.findDirectChildOfObj(obj);
  				if (obj instanceof Kekule.ChemStructureNode)
  					Kekule.ArrayUtils.pushUnique(nodes, obj);
  				else if (obj instanceof Kekule.ChemStructureConnector)
  				{
  					Kekule.ArrayUtils.pushUnique(connectors, obj);
  					var connected = Kekule.ChemStructureUtils._getCascadeConnectedNodesAndConnectors(obj);
  					Kekule.ArrayUtils.pushUnique(connectors, connected.connectors);
  					Kekule.ArrayUtils.pushUnique(nodes, connected.nodes);
  				}
  			}
  		}
  		return {'connectors': connectors, 'nodes': nodes};
  	},
  
  	/**
  	 * Merge all fragments into a big one (this one may be unconnected).
  	 * @param {Array} fragments
  	 * @param {Class} newFragmentClass If set, new fragment will be based on this class.
  	 *   Otherwise an instance of {@link Kekule.Molecule} will be created.
  	 * @return {Kekule.StructureFragment}
  	 */
  	mergeStructFragments: function(fragments, newFragmentClass)
  	{
  		if (fragments.length <= 1)
  			return fragments[0];
  		else
  		{
  			var fclass = newFragmentClass || Kekule.Molecule;
  			var result = new fclass();
  
  			for (var i = 0, l = fragments.length; i < l; ++i)
  			{
  				var frag = fragments[i].clone();
  				Kekule.ChemStructureUtils.moveChildBetweenStructFragment(frag, result, frag.getNodes(), frag.getConnectors());
  			}
  			return result;
  		}
  	},
  
  	/**
  	 * Split structFragment with unconnected nodes to multiple ones.
  	 * @param {Kekule.StructureFragment} structFragment
  	 * @returns {Array}
  	 */
  	splitStructFragment: function(structFragment)
  	{
  		if (!structFragment.hasCtab())  // no ctab, can not split
  			return [structFragment];
  
  		var allNodes = structFragment.getNodes();
  		if (allNodes.length <= 0)
  			return [structFragment];
  
  		var allConnectors = structFragment.getConnectors();
  
  		var splits = [];
  		var currNodes = [allNodes[0]];
  		var currConnectors = [];
  		var currIndex = 0;
  		//while (currNodes.length < allNodes.length))
  		do
  		{
  			var node = currNodes[currIndex];
  			var connectors = node.getLinkedConnectors();
  			if (node.getCrossConnectors)
  				connectors.concat(node.getCrossConnectors() || []);
  			Kekule.ArrayUtils.pushUnique(currConnectors, connectors);
  			for (var i = 0, l = connectors.length; i < l; ++i)
  			{
  				var connected = Kekule.ChemStructureUtils._getCascadeConnectedNodesAndConnectors(connectors[i], structFragment);
  				Kekule.ArrayUtils.pushUnique(currConnectors, connected.connectors);
  				Kekule.ArrayUtils.pushUnique(currNodes, connected.nodes);
  			}
  			++currIndex;
  		}
  		while (currIndex < currNodes.length);
  
  		splits.push(structFragment);
  
  		var restNodes = Kekule.ArrayUtils.exclude(allNodes, currNodes);
  		var restConnectors = Kekule.ArrayUtils.exclude(allConnectors, currConnectors);
  
  		if (restNodes.length > 0)
  		{
  			var fragClass = structFragment.getClass();
  			var newFragment = new fragClass();
  			Kekule.ChemStructureUtils.moveChildBetweenStructFragment(structFragment, newFragment, restNodes, restConnectors);
  
  			var newSplits = Kekule.ChemStructureUtils.splitStructFragment(newFragment);
  			splits = splits.concat(newSplits);
  		}
  		return splits;
  	},
  
  	/**
  	 * Returns a vector reflect coord2 - coord1.
  	 * @param {Kekule.ChemStructureObject} obj1
  	 * @param {Kekule.ChemStructureObject} obj2
  	 * @param {Int} coordMode
  	 * @param {Bool} allowCoordBorrow
  	 * @returns {Hash}
  	 */
  	getAbsCoordVectorBetweenObjs: function(obj1, obj2, coordMode, allowCoordBorrow)
  	{
  		var coord1 = obj1.getAbsCoordOfMode(coordMode, allowCoordBorrow);
  		var coord2 = obj2.getAbsCoordOfMode(coordMode, allowCoordBorrow);
  		return Kekule.CoordUtils.substract(coord2, coord1);
  	},
  
  	/** @private */
  	_getRef2DCoordOfObj: function(obj, allowCoordBorrow)
  	{
  		var coord = obj.getAbsBaseCoord2D? obj.getAbsBaseCoord2D(allowCoordBorrow):
  				obj.getAbsCoord2D? obj.getAbsCoord2D(allowCoordBorrow): obj.getCoord2D(allowCoordBorrow);
  		return coord;
  	},
  	/** @private */
  	_getStandardizedLinkedObj2DRelCoords: function(baseObj, excludeObjs, allowCoordBorrow, includeAttachedMarkers, includeUnexposedSiblings)
  	{
  		var result = [];
  		var baseCoord = Kekule.ChemStructureUtils._getRef2DCoordOfObj(baseObj, allowCoordBorrow);
  		var linkedObjs = includeUnexposedSiblings? baseObj.getLinkedObjs(): baseObj.getLinkedExposedObjs();
  
  		if (includeAttachedMarkers && baseObj.getAttachedMarkers)
  		{
  			linkedObjs = linkedObjs.concat(baseObj.getAttachedMarkers() || []);
  		}
  
  		if (excludeObjs && excludeObjs.length)
  			linkedObjs = AU.exclude(linkedObjs, excludeObjs);
  
  		for (var i = 0, l = linkedObjs.length; i < l; ++i)
  		{
  			var obj = linkedObjs[i];
  
  			var coord = Kekule.ChemStructureUtils._getRef2DCoordOfObj(obj, allowCoordBorrow);
  			var newCoord = Kekule.CoordUtils.substract(coord, baseCoord);
  			newCoord = Kekule.CoordUtils.standardize(newCoord);
  			result.push(newCoord);
  		}
  		return result;
  	},
  	/**
  	 * Returns an array of connector (and attachedMarkers) angles ( to X-axis ) of object.
  	 * @returns {Array}
  	 * @private
  	 */
  	_calcLinkedObj2DAnglesOfObj: function(baseObj, excludeObjs, allowCoordBorrow, includeAttachedMarkers, includeUnexposedSiblings)
  	{
  		var result = [];
  		var linkedObjCoords = Kekule.ChemStructureUtils._getStandardizedLinkedObj2DRelCoords(baseObj, excludeObjs, allowCoordBorrow, includeAttachedMarkers, includeUnexposedSiblings);
  		for (var i = 0, l = linkedObjCoords.length; i < l; ++i)
  		{
  			var c = linkedObjCoords[i];
  			if (c.x === 0 && c.y === 0)  // zero coord, bypass
  				continue;
  			var angle = Math.atan2(c.y, c.x);
  			if (angle < 0)
  				angle = Math.PI * 2 + angle;
  			result.push(angle);
  		}
  		result.sort();
  		return result;
  	},
  	/** @private */
  	_getMostEmptyDirectionOfExistingAngles: function(angles)  // angles must be sorted first
  	{
  		var l = angles.length;
  		if (l === 0)
  			return 0;
  		else if (l === 1)  // only one connector
  			return Kekule.GeometryUtils.standardizeAngle(Math.PI + angles[0]);
  		else  // more than two connectors
  		{
  			var max = 0;
  			var index = 0;
  			for (var i = 0; i < l; ++i)
  			{
  				var a1 = angles[i];
  				var a2 = angles[(i + 1) % l];
  				var delta = a2 - a1;
  				if (delta < 0)
  					delta += Math.PI * 2;
  				if (delta > max)
  				{
  					max = delta;
  					index = i;
  				}
  			}
  			var result = angles[index] + max / 2;
  			/* debug
  			var msg = 'Angles: [';
  			for (var i = 0; i < l; ++i)
  				msg += (angles[i] * 180 / Math.PI) + ' '
  			msg + ']';
  			console.log(msg, result * 180 / Math.PI);
        */
  			return result;
  		}
  	},
  	/**
  	 * Get the emptiest 2D direction around obj. Returns angle of that direction.
  	 * @returns {Float}
  	 * @private
  	 */
  	getMostEmptyDirection2DAngleOfObj: function(baseObj, excludeObjs, allowCoordBorrow, includeAttachedMarkers, includeUnexposedSiblings, avoidDirectionAngles)
  	{
  		var angles = Kekule.ChemStructureUtils._calcLinkedObj2DAnglesOfObj(baseObj, excludeObjs, allowCoordBorrow, includeAttachedMarkers, includeUnexposedSiblings);
  		if (avoidDirectionAngles)
  		{
  			angles = angles.concat(avoidDirectionAngles);
  			angles.sort();
  		}
  		var result = Kekule.ChemStructureUtils._getMostEmptyDirectionOfExistingAngles(angles);
  		//console.log('angle', result * 180 / Math.PI, Math.cos(result), Math.sin(result));
  		return result;
  	},
  
  	/**
  	 * Check if two rings are the same. Param ring1/ring2 are object wth fields: {nodes, connectors}
  	 * @param {Object} ring1
  	 * @param {Object} ring2
  	 * @returns {Bool}
  	 */
  	isSameRing: function(ring1, ring2)
  	{
  		if (ring1.nodes.length !== ring2.nodes.length)
  			return false;
  		if (ring1.connectors.length !== ring2.connectors.length)
  			return false;
  		if (AU.exclude(ring1.nodes, ring2.nodes).length > 0)
  			return false;
  		if (AU.exclude(ring1.connectors, ring2.connectors).length > 0)
  			return false;
  		return true;
  	},
  
  	/**
  	 * Check if targetRing is in rings.
  	 * @param {Object} targetRing
  	 * @param {Array} rings
  	 * @returns false;
  	 */
  	isInRings: function(targetRing, rings)
  	{
  		for (var i = 0, l = rings.length; i < l; ++i)
  		{
  			var ring = rings[i];
  			if (targetRing === ring)
  				return true;
  			else if (Kekule.ChemStructureUtils.isSameRing(targetRing, ring))
  				return true;
  		}
  		return false;
  	},
  
  	/**
  	 * Guess and returns the implicit hydrogen count connected to an atom.
  	 * @param {Int} atomicNum
  	 * @param {Hash} params Additional params, may including fields {coValenceBondValenceSum, otherBondValenceSum, charge, radicalECount}
  	 * @returns {Int}
  	 */
  	getImplicitHydrogenCount: function(atomicNum, params)
  	{
  		var p = Object.extend({coValenceBondValenceSum: 0, otherBondValenceSum: 0, charge: 0, radicalECount: 0}, params || {}, true);
  		var valence = Kekule.ValenceUtils.getImplicitValence(atomicNum, p.charge, p.coValenceBondValenceSum);
  		valence -= p.radicalECount;
  		return Math.max(valence - p.coValenceBondValenceSum - p.otherBondValenceSum, 0);
  	}
  };
  
  
  /**
   * An abstract class to analysis and get tokens from text.
   * @augments ObjectEx
   * @class
   * @param {String} text Text to analysis.
   */
  Kekule.TokenAnalyzer = Class.create(ObjectEx,
  /** @lends Kekule.TokenAnalyzer# */
  {
  	/** @private */
  	CLASS_NAME: 'Kekule.TokenAnalyzer',
  	/**
  	 * @constructs
  	 */
  	initialize: function(/*$super, */text)
  	{
  		this.tryApplySuper('initialize')  /* $super() */;
  		this.setSrcText(text);
  	},
  	/** @private */
  	initProperties: function()
  	{
  		// private properties
  		this.defineProp('srcText', {'dataType': DataType.STRING,
  			'setter': function(value)
  			{
  				var v = value || '';
  				this.setPropStoreFieldValue('srcText', v);
  				this.setPropStoreFieldValue('srcLength', v.length);
  				this.setCurrPos(0);
  			}
  		});
  		this.defineProp('srcLength', {'dataType': DataType.INT, 'setter': null, 'serializable': false})
  		this.defineProp('currPos', {'dataType': DataType.INT, 'serializable': false});
  	},
  	/**
  	 * Returns type of char. Neighboring char with same type can be merged into a token.
  	 * Descendants need to override this method.
  	 * @param {String} c
  	 * @returns {Variant}
  	 * @private
  	 */
  	getCharType: function(c)
  	{
  		// do nothing here
  		return 0;
  	},
  	/**
  	 * Check if two char types are matched and can be merged into a token.
  	 * Descendants may override this method.
  	 * @param {Variant} currT
  	 * @param {Variant} lastT
  	 * @return {Bool}
  	 */
  	isCharTypeMatched: function(currT, lastT)
  	{
  		return currT === lastT;
  	},
  	/** @private */
  	nextCharInfo: function()
  	{
  		var p = this.getCurrPos();
  		if (p >= this.getSrcLength())
  			return null;
  		else
  		{
  			var c = this.getSrcText().charAt(p);
  			this.setCurrPos(p + 1);
  			return {'char': c, 'charType': this.getCharType(c)};
  		}
  	},
  	/**
  	 * Returns next token of srcText.
  	 * @returns {Hash} {token, tokenType}
  	 * @private
  	 */
  	nextTokenInfo: function()
  	{
  		var lastCharInfo = this.nextCharInfo();
  		if (lastCharInfo)
  		{
  			var token = lastCharInfo['char'];  // .char will cause problem in YUI compressor
  			var tokenType = lastCharInfo.charType;
  			var currCharInfo = this.nextCharInfo();
  			while (currCharInfo && this.isCharTypeMatched(currCharInfo.charType, lastCharInfo.charType))
  			{
  				token += currCharInfo['char'];
  				lastCharInfo = currCharInfo;
  				currCharInfo = this.nextCharInfo();
  			}
  			if (currCharInfo)  // now currCharInfo type is different from last, reverse a pos
  				this.setCurrPos(this.getCurrPos() - 1);
  			return {'token': token, 'tokenType': tokenType};
  		}
  		else
  			return null;
  	},
  	/**
  	 * Returns all token info in src text.
  	 * @returns {Array} Each item is a hash of {token, tokenType}.
  	 */
  	getAllTokenInfos: function()
  	{
  		var result = [];
  		var info = this.nextTokenInfo();
  		while (info)
  		{
  			result.push(info);
  			info = this.nextTokenInfo();
  		}
  		return result;
  	}
  });
  
  /**
   * Enumeration of chem text token type.
   * @enum
   */
  Kekule.ChemTextTypes = {
  	// Chem text char types
  	/** @private */
  	CT_ATOM_SYMBOL_LEADING: 1,  // highercased alphabet, e.g. 'C' in 'Cu'
  	/** @private */
  	CT_ATOM_SYMBOL_FOLLOWING: 2,  // lowercased alphabet, e.g. 'u' in 'Cu'
  	/** @private */
  	CT_CHARGE_SYMBOL: 3,  // '+', '-'
  	/** @private */
  	CT_NUMBER: 4,
  	/** @private */
  	CT_BRACKET_LEADING: 10,  // '(', '[' and '{'
  	/** @private */
  	CT_BRACKET_TAILING: 11,  // ')', ']' and '}'
  	/** @private */
  	CT_SEPARATOR: 20,  // space to separate texts
  	/** @private */
  	CT_UNKNOWN: 0
  };
  var CT = Kekule.ChemTextTypes;
  
  /**
   * A helper class to analysis chem text (e.g. formula).
   * @augments Kekule.TokenAnalyzer
   * @class
   * @param {String} text Text to analysis.
   */
  Kekule.ChemTextAnalyzer = Class.create(Kekule.TokenAnalyzer,
  /** @lends Kekule.ChemTextAnalyzer# */
  {
  	/** @private */
  	CLASS_NAME: 'Kekule.ChemTextAnalyzer',
  	/** @ignore */
  	getCharType: function(c)
  	{
  		if (c === ' ')
  			return CT.CT_SEPARATOR;
  		if (c >= '0' && c <= '9')  // number
  			return CT.CT_NUMBER;
  		else if (['+', '-'].indexOf(c) >= 0)
  			return CT.CT_CHARGE_SYMBOL;
  		else if (['(', '[', '{'].indexOf(c) >= 0)
  			return CT.CT_BRACKET_LEADING;
  		else if ([')', ']', '}'].indexOf(c) >= 0)
  			return CT.CT_BRACKET_TAILING;
  		else if (c >= 'A' && c <= 'Z')
  			return CT.CT_ATOM_SYMBOL_LEADING;
  		else if (c >= 'a' && c <= 'z')
  			return CT.CT_ATOM_SYMBOL_FOLLOWING;
  		else
  			return CT.CT_UNKNOWN;
  	},
  	/** @ignore */
  	isCharTypeMatched: function(currT, lastT)
  	{
  		if (Kekule.ArrayUtils.intersect([CT.CT_BRACKET_LEADING, CT.CT_BRACKET_TAILING], [currT, lastT]).length)
  			return false;
  		else
  			return (currT === lastT && lastT !== CT.CT_ATOM_SYMBOL_LEADING) ||
  				(lastT === CT.CT_ATOM_SYMBOL_LEADING && currT === CT.CT_ATOM_SYMBOL_FOLLOWING);
  	}
  });
  
  /**
   * Util class to manipulate molecule formulas.
   * @class
   */
  Kekule.FormulaUtils = {
  	/**
  	 * Nestable brackets used to display formula.
  	 * @private
  	 */
  	FORMULA_BRACKETS: [['(', ')'], ['[', ']'], ['{', '}']],
  	/** @private */
  	FORMULA_BRACKET_TYPE_COUNT: 3,
  	/**
  	 * Create a formula object from plain text.
  	 * @param {String} text
  	 * @param {Kekule.ChemObject} parent Parent object of formula.
  	 * @param {Kekule.MoleculeFormula} formula If this param is set, changes will be take on this object.
  	 *   Otherwise a new instance of formula will be created and returned.
  	 * @returns {Kekule.MoleculeFormula}
  	 */
  	textToFormula: function(text, parent, formula)
  	{
  		var result = formula || null;
  		if (result)
  		{
  			result.clear();
  		}
  		var analyzer = new Kekule.ChemTextAnalyzer(text);
  		try
  		{
  			var tokenInfos = analyzer.getAllTokenInfos();
  			//console.log(tokenInfos);
  			if (!result)
  				result = new Kekule.MolecularFormula(parent);
  			var tokenLength =  tokenInfos.length;
  			if (tokenLength)
  			{
  				var currObj, currSection, lastTokenInfo = null, currCharge;
  				var currFormula = result;
  				currSection = {};
  
  				var createNewSection = function()
  				{
  					currSection = {};
  				};
  				var wrapUpCurrSection = function()
  				{
  					if (currSection && currSection.obj)
  						currFormula.appendSection(currSection.obj, currSection.count, currSection.charge);
  				};
  
  				// iterate through tokens
  				for (var i = 0; i < tokenLength; ++i)
  				{
  					var tokenInfo = tokenInfos[i];
  					var tokenType = tokenInfo.tokenType;
  					var token = tokenInfo.token;
  					var handled = false;   // mark if currToken is handled
  					if (tokenType === CT.CT_BRACKET_LEADING)  // bracket, new layer
  					{
  						wrapUpCurrSection();
  						var subFormula = new Kekule.MolecularFormula(parent);
  						subFormula._parentFormula = currFormula;
  						currFormula = subFormula;
  						createNewSection();
  						handled = true;
  					}
  					else if (tokenType === CT.CT_BRACKET_TAILING)
  					{
  						wrapUpCurrSection();
  						createNewSection();
  						currSection.obj = currFormula;
  						currFormula = currFormula._parentFormula;
  						tokenInfo.asSymbol = true;
  						handled = true;
  					}
  					else if ([CT.CT_ATOM_SYMBOL_LEADING, CT.CT_ATOM_SYMBOL_FOLLOWING].indexOf(tokenType) >= 0)  // atom symbol
  					{
  						if (currSection.obj)
  						{
  							wrapUpCurrSection();
  							createNewSection();
  						}
  						// TODO: currently only atom can be created
  						var massNum = currSection.massNum;
  						if (!massNum)  // check if lastToken is unhandled number, if so, it may be the mass num of current symbol
  						{
  							if (lastTokenInfo && !lastTokenInfo.handled && (lastTokenInfo.tokenType === CT.CT_NUMBER))
  								massNum = parseInt(lastTokenInfo.token, 10) || null;
  						}
  						var slabel = '' + (massNum || '') + token;
  						currSection.obj = Kekule.ChemStructureNodeFactory.createByLabel(slabel);
  						//currSection.obj = new Kekule.Atom(null, token, massNum);
  						tokenInfo.asSymbol = true;
  						handled = true;
  					}
  					else if (tokenType === CT.CT_CHARGE_SYMBOL)
  					{
  						currCharge = (token === '+')? +1: -1;
  						tokenInfo.asCharge = true;
  						if (lastTokenInfo.tokenType === CT.CT_NUMBER)
  						{
  							var schargeCount;
  							if (lastTokenInfo.asCount)
  							{
  								if (lastTokenInfo.token.length > 1)  // last digit should be charge
  								{
  									var t = lastTokenInfo.token;
  									schargeCount = t.substr(t.length - 1);
  									if (lastTokenInfo.asCount)
  										currSection.count = parseInt(t.substring(0, t.length - 1), 10);
  								}
  								else
  								{
  									schargeCount = lastTokenInfo.token;
  									if (lastTokenInfo.asCount)
  										currSection.count = 1;
  								}
  							}
  							else
  							{
  								schargeCount = lastTokenInfo.token;
  								lastTokenInfo.asChargeCount = true;
  							}
  							currCharge *= parseInt(schargeCount, 10);
  						}
  						currSection.charge = currCharge;
  						handled = true;
  					}
  					else if (tokenType === CT.CT_NUMBER)
  					{
  						var num = parseInt(token, 10);
  						if (currCharge && lastTokenInfo.tokenType === CT.CT_CHARGE_SYMBOL && lastTokenInfo.asCharge)  // last token is charge
  						{
  							currCharge *= num;
  							currSection.charge = currCharge;
  							tokenInfo.asChargeCount = true;
  							handled = true;
  						}
  						else if (currSection.obj && lastTokenInfo.asSymbol)  // atom symbol already set, number is count
  						{
  							currSection.count = num;
  							tokenInfo.asCount = true;
  							handled = true;
  						}
  						else if (!currSection.obj)  // symbol not set, this leading number should be the isotope number
  						{
  							currSection.massNum = num;
  							handled = true;
  						}
  						else  // do not know the use of number, may be the mass num of next atom symbol
  						{
  
  						}
  					}
  					tokenInfo.handled = handled;
  					lastTokenInfo = tokenInfo;
  				}
  
  				if (currSection && currSection.obj)
  					wrapUpCurrSection();
  			}
  			// debug
  			//var rt = result.getDisplayRichText();
  			//console.log(Kekule.Render.RichTextUtils.toText(rt));
  			return result;
  		}
  		finally
  		{
  			analyzer.finalize();
  		}
  	},
  
  	/**
  	 * Returns plain text generated by formula.
  	 * @param {Kekule.MolculeFormula} formula
  	 * @param {Bool} showCharge Whether display formula charge.
  	 * @param {Int} partialChargeDecimalsLength
  	 * @returns {String}
  	 */
  	formulaToText: function(formula, showCharge, partialChargeDecimalsLength)
  	{
  		/*
  		var result = '';
  		var sections = formula.getSections();
  		*/
  		/*
  		var rt = Kekule.Render.ChemDisplayTextUtils.formulaToRichText(formula, true, true);
  		var result = Kekule.Render.RichTextUtils.toText(rt);
  		return result;
  		*/
  		if (Kekule.ObjUtils.isUnset(showCharge))
  			showCharge = true;
  		return FU._convFormulaToText(formula, false, showCharge, partialChargeDecimalsLength);
  	},
  
  	/** @private */
  	_convFormulaToText: function(formula, showBracket, showCharge, partialChargeDecimalsLength)
  	{
  		var result = '';
  		var sections = formula.getSections();
  		if (showBracket)
  		{
  			var bracketIndex = formula.getMaxNestedLevel() % FU.FORMULA_BRACKET_TYPE_COUNT;
  			var bracketStart =FU.FORMULA_BRACKETS[bracketIndex][0];
  			var bracketEnd = FU.FORMULA_BRACKETS[bracketIndex][1];
  			result += bracketStart;
  		}
  		for (var i = 0, l = sections.length; i < l; ++i)
  		{
  			var obj = sections[i].obj;
  			var charge = formula.getSectionCharge(sections[i]);
  			var subgroup = null;
  			if (obj instanceof Kekule.MolecularFormula)  // a sub-formula
  			{
  				// TODO: sometimes bracket is unessential, such as SO42- and so on, need more judge here
  				subgroup = FU._convFormulaToText(obj, true, false, false, partialChargeDecimalsLength); // do not show charge right after, we will add it later
  			}
  			else if (obj.getLabel) // an atom/isotope
  			{
  				var subgroup = obj.getLabel();
  				if (obj.getMassNumber && obj.getMassNumber())  // explicit mass number atom, add a separator before it
  					subgroup = (result? ' ': '') + subgroup;
  			}
  
  			if (subgroup)
  			{
  				var explicitCount = false;
  				// count
  				if (sections[i].count != 1)
  				{
  					subgroup += sections[i].count;
  					explicitCount = true;
  				}
  
  				// charge is draw after count
  				if (showCharge && charge)
  				{
  					var chargelabel = FU._convChargeToText(charge, partialChargeDecimalsLength);
  					//chargelabel += chargeSign;
  					subgroup += (explicitCount? ' ': '') + chargelabel;  // separate count and charge
  				}
  
  				result += subgroup;
  			}
  		}
  		if (showBracket)
  			result += bracketEnd;
  
  		if (showCharge)
  		{
  			var charge = formula.getCharge();
  			if (charge)
  			{
  				var chargelabel = FU._convChargeToText(charge, partialChargeDecimalsLength);
  				result += chargelabel;
  			}
  		}
  
  		return result;
  	},
  	/** @private */
  	_convChargeToText: function(charge, partialChargeDecimalsLength)
  	{
  		if (!charge)
  			return null;
  		var chargeSign = (charge > 0)? '+': '-';
  		var chargeAmount = Math.abs(charge);
  		var chargelabel = chargeSign;
  		if (chargeAmount != 1)
  		{
  			chargelabel = (partialChargeDecimalsLength? Kekule.NumUtils.toDecimals(chargeAmount, partialChargeDecimalsLength): chargeAmount.toString()) + chargeSign;
  		}
  		return chargelabel;
  	}
  };
  
  var FU = Kekule.FormulaUtils;
  
  // extend MoleculeFormula class
  ClassEx.defineProp(Kekule.MolecularFormula, 'text', {
  	'dataType': DataType.STRING, 'serializable': false,
  	'getter': function() { return FU.formulaToText(this); },
  	'setter': function(value)
  	{
  		FU.textToFormula(value, this.getParent(), this);
  	}
  });
  
  })();
  