# -*- coding: utf-8 -*-
#
# AWL data types
#
# Copyright 2012-2017 Michael Buesch <m@bues.ch>
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

from __future__ import division, absolute_import, print_function, unicode_literals
#from awlsim.common.cython_support cimport * #@cy
from awlsim.common.compat import *

from awlsim.common.datatypehelpers import * #+cimport
from awlsim.common.immutable import *
from awlsim.common.enumeration import *
from awlsim.common.exceptions import *
from awlsim.common.util import *

from awlsim.core.timers import * #+cimport
from awlsim.core.offset import * #+cimport
from awlsim.core.identifier import *

#from awlsim.core.memory cimport * #@cy

import datetime


__all__ = [
	"AwlDataType",
]


class AwlDataType(OptionalImmutable):
	# Data type IDs
	EnumGen.start
	TYPE_VOID	= EnumGen.item
	TYPE_NIL	= EnumGen.item
	TYPE_BOOL	= EnumGen.item
	TYPE_BYTE	= EnumGen.item
	TYPE_WORD	= EnumGen.item
	TYPE_DWORD	= EnumGen.item
	TYPE_INT	= EnumGen.item
	TYPE_DINT	= EnumGen.item
	TYPE_REAL	= EnumGen.item
	TYPE_S5T	= EnumGen.item
	TYPE_TIME	= EnumGen.item
	TYPE_DATE	= EnumGen.item
	TYPE_DT		= EnumGen.item
	TYPE_TOD	= EnumGen.item
	TYPE_CHAR	= EnumGen.item
	TYPE_ARRAY	= EnumGen.item
	TYPE_STRUCT	= EnumGen.item
	TYPE_STRING	= EnumGen.item
	TYPE_TIMER	= EnumGen.item
	TYPE_COUNTER	= EnumGen.item
	TYPE_POINTER	= EnumGen.item
	TYPE_ANY	= EnumGen.item
	TYPE_BLOCK_DB	= EnumGen.item # DB number type
	TYPE_BLOCK_SDB	= EnumGen.item # SDB number type
	TYPE_BLOCK_FB	= EnumGen.item # FB number type
	TYPE_BLOCK_FC	= EnumGen.item # FC number type
	TYPE_DB_X	= EnumGen.item # DBx type
	TYPE_OB_X	= EnumGen.item # OBx type
	TYPE_FC_X	= EnumGen.item # FCx type
	TYPE_SFC_X	= EnumGen.item # SFCx type
	TYPE_FB_X	= EnumGen.item # FBx type
	TYPE_SFB_X	= EnumGen.item # SFBx type
	TYPE_UDT_X	= EnumGen.item # UDTx type
	TYPE_VAT_X	= EnumGen.item # VATx type
	EnumGen.end

	__name2id = {
		"VOID"		: TYPE_VOID,
		"NIL"		: TYPE_NIL,
		"BOOL"		: TYPE_BOOL,
		"BYTE"		: TYPE_BYTE,
		"WORD"		: TYPE_WORD,
		"DWORD"		: TYPE_DWORD,
		"INT"		: TYPE_INT,
		"DINT"		: TYPE_DINT,
		"REAL"		: TYPE_REAL,
		"S5TIME"	: TYPE_S5T,
		"TIME"		: TYPE_TIME,
		"DATE"		: TYPE_DATE,
		"DATE_AND_TIME"	: TYPE_DT,
		"TIME_OF_DAY"	: TYPE_TOD,
		"CHAR"		: TYPE_CHAR,
		"ARRAY"		: TYPE_ARRAY,
		"STRUCT"	: TYPE_STRUCT,
		"STRING"	: TYPE_STRING,
		"TIMER"		: TYPE_TIMER,
		"COUNTER"	: TYPE_COUNTER,
		"POINTER"	: TYPE_POINTER,
		"ANY"		: TYPE_ANY,
		"BLOCK_DB"	: TYPE_BLOCK_DB,
		"BLOCK_SDB"	: TYPE_BLOCK_SDB,
		"BLOCK_FB"	: TYPE_BLOCK_FB,
		"BLOCK_FC"	: TYPE_BLOCK_FC,
		"DB"		: TYPE_DB_X,
		"OB"		: TYPE_OB_X,
		"FC"		: TYPE_FC_X,
		"SFC"		: TYPE_SFC_X,
		"FB"		: TYPE_FB_X,
		"SFB"		: TYPE_SFB_X,
		"UDT"		: TYPE_UDT_X,
		"VAT"		: TYPE_VAT_X,
	}
	__id2name = pivotDict(__name2id)

	# Width table for types
	# -1 => Type width must be calculated
	typeWidths = {
		TYPE_VOID	: 0,
		TYPE_NIL	: 0,
		TYPE_BOOL	: 1,
		TYPE_BYTE	: 8,
		TYPE_WORD	: 16,
		TYPE_DWORD	: 32,
		TYPE_INT	: 16,
		TYPE_DINT	: 32,
		TYPE_REAL	: 32,
		TYPE_S5T	: 16,
		TYPE_TIME	: 32,
		TYPE_DATE	: 16,
		TYPE_DT		: 64,
		TYPE_TOD	: 32,
		TYPE_CHAR	: 8,
		TYPE_ARRAY	: -1,
		TYPE_STRUCT	: -1,
		TYPE_STRING	: -1,
		TYPE_TIMER	: 16,
		TYPE_COUNTER	: 16,
		TYPE_POINTER	: 48,
		TYPE_ANY	: 80,
		TYPE_BLOCK_DB	: 16,
		TYPE_BLOCK_SDB	: 16,
		TYPE_BLOCK_FB	: 16,
		TYPE_BLOCK_FC	: 16,
		TYPE_DB_X	: -1,
		TYPE_OB_X	: 0,
		TYPE_FC_X	: 0,
		TYPE_SFC_X	: 0,
		TYPE_FB_X	: -1,
		TYPE_SFB_X	: -1,
		TYPE_UDT_X	: -1,
		TYPE_VAT_X	: 0,
	}

	# Table of trivial types with sign
	__signedTypes = {
		TYPE_INT,
		TYPE_DINT,
		TYPE_REAL,
	}

	# Table of compound types
	__compoundTypes = {
		TYPE_DT,
		TYPE_ARRAY,
		TYPE_STRUCT,
		TYPE_STRING,
		TYPE_UDT_X,

		# No TYPE_POINTER here.
		# Technically POINTER is compound, too, but we use this
		# table to decide whether we need to create POINTERs.
		# Adding TYPE_POINTER here would create an infinite loop.
	}

	# Table of types allowed inside of STRUCT.
	__allowedInStructTypes = {
		TYPE_BOOL,
		TYPE_BYTE,
		TYPE_WORD,
		TYPE_DWORD,
		TYPE_INT,
		TYPE_DINT,
		TYPE_REAL,
		TYPE_S5T,
		TYPE_TIME,
		TYPE_DATE,
		TYPE_DT,
		TYPE_TOD,
		TYPE_CHAR,
		TYPE_ARRAY,
		TYPE_STRUCT,
		TYPE_STRING,
		TYPE_UDT_X,
	}

	# Table of types allowed as ARRAY element.
	__allowedInArrayTypes = __allowedInStructTypes

	# Table of types allowed as block interface element.
	__allowedInInterfaceTypes = __allowedInStructTypes | {
		TYPE_TIMER,
		TYPE_COUNTER,
		TYPE_POINTER,
		TYPE_ANY,
		TYPE_BLOCK_DB,
		TYPE_BLOCK_FB,
		TYPE_BLOCK_FC,
		TYPE_FB_X,
		TYPE_SFB_X,
	}

	# Data-types that must be passed "by-reference" to FCs/FBs.
	callByRefTypes = {
		TYPE_TIMER,
		TYPE_COUNTER,
		TYPE_BLOCK_DB,
		TYPE_BLOCK_FB,
		TYPE_BLOCK_FC,
	}

	# Convert a list of array dimensions into a number of elements.
	@classmethod
	def arrayDimensionsToNrElements(cls, dimensions):
		assert(dimensions is not None)
		assert(len(dimensions) >= 1 and len(dimensions) <= 6)
		nrElems = reduce(lambda a, b: a * (b[1] - b[0] + 1),
				 dimensions, 1)
		return nrElems

	@classmethod
	def _name2typeid(cls, nameTokens):
		nameTokens = toList(nameTokens)
		try:
			return cls.__name2id[nameTokens[0].upper()]
		except (KeyError, IndexError) as e:
			raise AwlSimError("Invalid data type name: " +\
					  nameTokens[0] if len(nameTokens) else "None")

	# Dict of simple types that were already contructed
	# and can be re-used.
	# All types herein are forced immutable.
	__staticTypes = {
		TYPE_VOID	: None,
		TYPE_BOOL	: None,
		TYPE_BYTE	: None,
		TYPE_WORD	: None,
		TYPE_DWORD	: None,
		TYPE_INT	: None,
		TYPE_DINT	: None,
		TYPE_REAL	: None,
		TYPE_S5T	: None,
		TYPE_TIME	: None,
		TYPE_DATE	: None,
		TYPE_DT		: None,
		TYPE_TOD	: None,
		TYPE_CHAR	: None,
		TYPE_TIMER	: None,
		TYPE_COUNTER	: None,
		TYPE_POINTER	: None,
		TYPE_ANY	: None,
		TYPE_BLOCK_DB	: None,
		TYPE_BLOCK_SDB	: None,
		TYPE_BLOCK_FB	: None,
		TYPE_BLOCK_FC	: None,
	}

	@classmethod
	def makeByName(cls, nameTokens, arrayDimensions=None):
		"""Make an AwlDataType instance by type name.
		nameTokens -> a list of tokens for the type name.
		arrayDimensions -> List of possible array dimensions, or None.
		                   Each list element is a tuple of (start, end)
				   with the start and end array index for that dimension."""

		nameTokens = toList(nameTokens)
		type = cls._name2typeid(nameTokens)

		# Check if we already had such a type and use that.
		if not arrayDimensions:
			dataType = cls.__staticTypes.get(type)
			if dataType:
				assert(dataType.isImmutable())
				return dataType

		if type == cls.TYPE_STRING:
			# Construct a data structure for the STRING layout.
			strLen = -1
			if len(nameTokens) == 1:
				# Extension: No dimensions means [254]
				strLen = 254
			elif len(nameTokens) == 4 and\
			     nameTokens[1] == '[' and nameTokens[3] == ']' and\
			     isdecimal(nameTokens[2]):
				strLen = int(nameTokens[2], 10)
			if strLen < 0 or strLen > 254:
				raise AwlSimError("Invalid STRING length definition "
					"in '%s'" % "".join(nameTokens))
			byteType = cls.makeByName("BYTE")
			import awlsim.core.datastructure as datastructure
			struct = datastructure.AwlStruct()
			struct.addField(cpu = None, name = None, dataType = byteType,
					initBytes = bytearray((strLen,)))
			struct.addField(cpu = None, name = None, dataType = byteType)
			for i in range(strLen):
				struct.addField(cpu = None, name = "[%s]" % (i + 1),
						dataType = byteType)
		else:
			struct = None

		index = None
		if type == cls.TYPE_ARRAY:
			raise AwlSimError("Nested ARRAYs are not allowed")
		elif type in (cls.TYPE_DB_X,
			      cls.TYPE_OB_X,
			      cls.TYPE_FC_X,
			      cls.TYPE_SFC_X,
			      cls.TYPE_FB_X,
			      cls.TYPE_SFB_X,
			      cls.TYPE_UDT_X):
			if len(nameTokens) < 2:
				raise AwlSimError("Invalid '%s' block data type" %\
					nameTokens[0])
			blockNumber = cls.tryParseImmediate_INT(nameTokens[1],
								maxLim=0xFFFF)
			if blockNumber is None:
				raise AwlSimError("Invalid '%s' block data type "\
					"index" % nameTokens[0])
			index = blockNumber

		if arrayDimensions:
			# An ARRAY is to be constructed.
			elementType = cls.makeByName(nameTokens)
			dataType = cls(type = cls.TYPE_ARRAY,
				       index = index,
				       arrayDimensions = arrayDimensions,
				       arrayElementType = elementType)
		else:
			dataType = cls(type = type,
				       index = index,
				       struct = struct)

			# Store the data type instance for later re-use, if it is
			# in the simple-types dict.
			# Force it immutable, so it cannot change anymore.
			if type in cls.__staticTypes:
				dataType.setImmutable()
				assert(dataType.index is None)
				assert(dataType.arrayDimensions is None)
				assert(dataType.arrayElementType is None)
				assert(dataType._struct is None)
				assert(cls.__staticTypes[type] is None)
				cls.__staticTypes[type] = dataType

		return dataType

	__slots__ = (
		"type",
		"index",
		"arrayDimensions",
		"arrayElementType",
		"_struct",
		"__widthOverride",
	)

	def __init__(self, type,
		     index=None,
		     arrayDimensions=None,
		     arrayElementType=None,
		     struct=None):
		super(AwlDataType, self).__init__()
		self.type = type		# The TYPE_... for this datatype
		self.index = index		# The Index number, if any. May be None
		self.arrayDimensions = arrayDimensions # The ARRAY's dimensions
		self.arrayElementType = arrayElementType # AwlDataType for the array elements.
		self.setStruct(struct)		# AwlStruct instance. Only for STRUCT type.
		self.__widthOverride = None

	def __eq__(self, other):
		return (self is other) or (\
			isinstance(other, AwlDataType) and\
			self.type == other.type and\
			self.index == other.index and\
			self.arrayDimensions == other.arrayDimensions and\
			self.arrayElementType == other.arrayElementType and\
			self._struct == other._struct and\
			self.__widthOverride == other.__widthOverride\
		)

	def __ne__(self, other):
		return not self.__eq__(other)

	# Set the AwlStruct that defines the structure of this STRUCT type.
	def setStruct(self, struct):
		assert(struct is None or
		       self.type == self.TYPE_STRUCT or
		       self.type == self.TYPE_UDT_X or
		       self.type == self.TYPE_STRING)
		self._struct = struct

	# Get the type element structure.
	# This is the element's struct for ARRAYs and the struct
	# of this type for STRUCTs/UDTs.
	# None for others.
	@property
	def itemStruct(self):
		if self.type == self.TYPE_ARRAY:
			return self.arrayElementType._struct
		return self._struct

	# Returns the width of this data type, in bits.
	@property
	def width(self):
		if self.__widthOverride is not None:
			return self.__widthOverride
		if self.type == self.TYPE_ARRAY:
			nrElements = self.arrayGetNrElements()
			if self.arrayElementType.type == self.TYPE_STRUCT:
				if self.arrayElementType._struct:
					oneElemWidth = self.arrayElementType._struct.getSize() * 8
					width = nrElements * oneElemWidth
				else:
					width = -1
			else:
				oneElemWidth = self.arrayElementType.width
				width = nrElements * oneElemWidth
		elif self.type == self.TYPE_STRUCT or\
		     self.type == self.TYPE_UDT_X:
			if self._struct:
				width = self._struct.getSize() * 8
			else:
				width = -1
		elif self.type == self.TYPE_STRING:
			width = self._struct.getUnalignedSize() * 8
		else:
			width = self.typeWidths[self.type]
		return width

	# Override the width calculation.
	@width.setter
	def width(self, widthOverride):
		self.__widthOverride = widthOverride

	# Returns True, if this is a compound data type.
	# Does not return True for TYPE_POINTER.
	@property
	def compound(self):
		return self.type in self.__compoundTypes

	# Returns True, if this is a signed type.
	@property
	def isSigned(self):
		return self.type in self.__signedTypes

	# Returns True, if this type is allowed in STRUCTs.
	@property
	def allowedInStruct(self):
		return self.type in self.__allowedInStructTypes

	# Returns True, if this type is allowed as ARRAY element.
	@property
	def allowedInArray(self):
		return self.type in self.__allowedInArrayTypes

	# Returns True, if this type is allowed in block interfaces.
	@property
	def allowedInInterface(self):
		return self.type in self.__allowedInInterfaceTypes

	# Possible values for 'naturalAlignment'.
	EnumGen.start
	ALIGN_BIT	= EnumGen.item
	ALIGN_BYTE	= EnumGen.item
	ALIGN_WORD	= EnumGen.item
	EnumGen.end

	# Get the natural alignment of this type.
	@property
	def naturalAlignment(self):
		if self.width == 1:
			return self.ALIGN_BIT
		if self.width == 8:
			return self.ALIGN_BYTE
		if self.type == self.TYPE_ARRAY or\
		   self.type == self.TYPE_STRUCT or\
		   self.type == self.TYPE_UDT_X or\
		   self.width > 8:
			return self.ALIGN_WORD
		assert(0)

	# Convert array indices into a one dimensional index for this array.
	# 'indices' is a list of index integers as written in the AWL operator
	# from left to right.
	def arrayIndicesCollapse(self, indices):
		if not indices:
			return None
		if len(indices) < 1 or len(indices) > 6:
			raise AwlSimError("Invalid number of array indices")
		assert(self.type == self.TYPE_ARRAY)
		signif = self.arrayIndexSignificances()
		assert(len(indices) == len(signif))
		assert(len(indices) == len(self.arrayDimensions))
		resIndex = 0
		for i, idx in enumerate(indices):
			startIdx, endIdx = self.arrayDimensions[i]
			if idx < startIdx or idx > endIdx:
				raise AwlSimError("Array index '%d' is out of bounds "
					"for array range '%d .. %d'." %\
					(idx, startIdx, endIdx))
			resIndex += (idx - startIdx) * signif[i]
		return resIndex

	# Get the array dimension sizes. Returns a tuple of integers.
	def arrayDimSizes(self):
		assert(self.type == self.TYPE_ARRAY)
		return tuple(end - start + 1
			     for start, end in self.arrayDimensions)

	# Get the array index significances. Returns a tuple of integers.
	def arrayIndexSignificances(self):
		assert(self.type == self.TYPE_ARRAY)
		sizes = self.arrayDimSizes()
		signif = [ 1, ]
		for size in list(sizes)[::-1][:-1]:
			signif.append(size * signif[-1])
		return tuple(signif[::-1])

	# Get the number of array elements
	def arrayGetNrElements(self):
		assert(self.type == self.TYPE_ARRAY)
		return self.arrayDimensionsToNrElements(self.arrayDimensions)

	# Parse an immediate, constrained by our datatype.
	def parseMatchingImmediate(self, tokens):
		typeId = self.type
		typeWidth = self.width
		if typeId == self.TYPE_ARRAY:
			typeId = self.arrayElementType.type
			typeWidth = self.arrayElementType.width

		value = None
		if tokens is None:
			value = 0
		elif len(tokens) == 10:
			if typeId == self.TYPE_DWORD:
				value, fields = self.tryParseImmediate_ByteArray(
							tokens)
		elif len(tokens) == 6:
			if typeId == self.TYPE_WORD:
				value, fields = self.tryParseImmediate_ByteArray(
							tokens)
		elif len(tokens) == 5:
			if typeId == self.TYPE_DT:
				value = self.tryParseImmediate_DT(tokens)
			elif typeId == self.TYPE_TOD:
				value = self.tryParseImmediate_TOD(tokens)
		elif len(tokens) == 2:
			if typeId == self.TYPE_TIMER:
				if tokens[0].upper() == "T":
					value = self.tryParseImmediate_INT(tokens[1],
									   minLim=0,
									   maxLim=0xFFFF)
			elif typeId == self.TYPE_COUNTER:
				if tokens[0].upper() in ("C", "Z"):
					value = self.tryParseImmediate_INT(tokens[1],
									   minLim=0,
									   maxLim=0xFFFF)
			elif typeId == self.TYPE_BLOCK_DB:
				if tokens[0].upper() == "DB":
					value = self.tryParseImmediate_INT(tokens[1],
									   minLim=0,
									   maxLim=0xFFFF)
			elif typeId == self.TYPE_BLOCK_FB:
				if tokens[0].upper() == "FB":
					value = self.tryParseImmediate_INT(tokens[1],
									   minLim=0,
									   maxLim=0xFFFF)
			elif typeId == self.TYPE_BLOCK_FC:
				if tokens[0].upper() == "FC":
					value = self.tryParseImmediate_INT(tokens[1],
									   minLim=0,
									   maxLim=0xFFFF)
		elif len(tokens) == 1:
			if typeId == self.TYPE_BOOL:
				value = self.tryParseImmediate_BOOL(
						tokens[0])
			elif typeId == self.TYPE_BYTE:
				value = self.tryParseImmediate_HexByte(
						tokens[0])
			elif typeId == self.TYPE_WORD:
				value = self.tryParseImmediate_Bin(
						tokens[0])
				if value is None:
					value = self.tryParseImmediate_HexWord(
							tokens[0])
				if value is None:
					value = self.tryParseImmediate_BCD_word(
							tokens[0])
			elif typeId == self.TYPE_DWORD:
				value = self.tryParseImmediate_Bin(
						tokens[0])
				if value is None:
					value = self.tryParseImmediate_HexDWord(
							tokens[0])
			elif typeId == self.TYPE_INT:
				value = self.tryParseImmediate_INT(
						tokens[0])
			elif typeId == self.TYPE_DINT:
				value = self.tryParseImmediate_DINT(
						tokens[0])
			elif typeId == self.TYPE_REAL:
				value = self.tryParseImmediate_REAL(
						tokens[0])
			elif typeId == self.TYPE_S5T:
				value = self.tryParseImmediate_S5T(
						tokens[0])
			elif typeId == self.TYPE_TIME:
				value = self.tryParseImmediate_TIME(
						tokens[0])
			elif typeId == self.TYPE_CHAR:
				value = self.tryParseImmediate_CHAR(
						tokens[0])
			elif typeId == self.TYPE_STRING:
				value = self.tryParseImmediate_STRING(
						tokens[0])
				typeStrLen = intDivRoundUp(typeWidth, 8) - 2
				immStrLen = len(value) - 2
				assert(typeStrLen >= 0 and immStrLen >= 0)
				if immStrLen > typeStrLen:
					raise AwlSimError("String immediate is "
						"too long. Length is %d characters, "
						"but maximum is %d characters." %\
						immStrLen, typeStrLen)
				# Expand the string to type length.
				if immStrLen < typeStrLen:
					value[0] = typeStrLen
					value.extend(b'\x00' * (typeStrLen - immStrLen))
			elif typeId == self.TYPE_DATE:
				value = self.tryParseImmediate_DATE(
						tokens[0])
		if value is None:
			raise AwlSimError("Immediate value '%s' does "
				"not match data type '%s'" %\
				(" ".join(tokens), str(self)))
		return value

	def __repr__(self):
		if self.type == self.TYPE_ARRAY:
			return "ARRAY" #TODO
		elif self.type == self.TYPE_STRING:
			return "STRING[%d]" % (self.width // 8 - 2)
		elif self.type in (self.TYPE_DB_X,
				   self.TYPE_OB_X,
				   self.TYPE_FC_X,
				   self.TYPE_SFC_X,
				   self.TYPE_FB_X,
				   self.TYPE_SFB_X,
				   self.TYPE_UDT_X):
			return "%s %d" % (
				self.__id2name[self.type],
				self.index,
			)
		try:
			return self.__id2name[self.type]
		except KeyError:
			raise AwlSimError("Invalid data type: " +\
					  str(type))

	@classmethod
	def tryParseImmediate_BOOL(cls, token):
		token = token.upper().strip()
		if token == "TRUE":
			return 1
		elif token == "FALSE":
			return 0
		return None

	@classmethod
	def tryParseImmediate_INT(cls, token, minLim=-32768, maxLim=32767):
		try:
			immediate = int(token, 10)
			if immediate > maxLim or immediate < minLim:
				raise AwlSimError("16-bit immediate overflow")
		except ValueError:
			return None
		return immediate

	@classmethod
	def tryParseImmediate_DINT(cls, token):
		token = token.upper()
		if not token.startswith("L#"):
			return None
		try:
			immediate = int(token[2:], 10)
			if immediate > 2147483647 or immediate < -2147483648: #+suffix-LL
				raise AwlSimError("32-bit immediate overflow")
			immediate &= 0xFFFFFFFF
		except ValueError as e:
			raise AwlSimError("Invalid immediate")
		return immediate

	@classmethod
	def tryParseImmediate_BCD_word(cls, token):
		token = token.upper()
		if not token.startswith("C#"):
			return None
		try:
			cnt = token[2:]
			if len(cnt) < 1 or len(cnt) > 3:
				raise ValueError
			a, b, c = 0, 0, 0
			if cnt:
				a = int(cnt[-1], 10)
				cnt = cnt[:-1]
			if cnt:
				b = int(cnt[-1], 10)
				cnt = cnt[:-1]
			if cnt:
				c = int(cnt[-1], 10)
				cnt = cnt[:-1]
			immediate = a | (b << 4) | (c << 8)
		except ValueError as e:
			raise AwlSimError("Invalid C# immediate")
		return immediate

	@classmethod
	def tryParseImmediate_REAL(cls, token):
		try:
			immFloat = float(token)
			immWord = pyFloatToDWord(immFloat)
		except ValueError:
			return None
		return immWord

	@classmethod
	def __parseGenericTime(cls, token, allowNegative):
		# Parse T# or S5T# time formats.
		# Returns the number of seconds as float.

		# Remove T# or S5T# prefix
		idx = token.find("#")
		if idx < 0:
			raise AwlSimError("Invalid time immediate: No prefix")
		token = token[idx+1 : ]

		# Remove underscores
		token = token.replace("_", "")

		if not token:
			raise AwlSimError("Invalid time immediate.")
		p = token.upper()

		isNegative = False
		if p.startswith("-"):
			if not allowNegative:
				raise AwlSimError("Negative time not allowed")
			isNegative = True
			p = p[1:]

		seconds = 0.0
		while p:
			if p.endswith("MS"):
				mult = 0.001
				p = p[:-2]
			elif p.endswith("S"):
				mult = 1.0
				p = p[:-1]
			elif p.endswith("M"):
				mult = 60.0
				p = p[:-1]
			elif p.endswith("H"):
				mult = 3600.0
				p = p[:-1]
			elif p.endswith("D"):
				mult = 86400.0
				p = p[:-1]
			else:
				raise AwlSimError("Invalid time immediate: "
					"Unknown unit suffix '%s'." % p)
			num = ""
			while p and p[-1] in "0123456789":
				num = p[-1] + num
				p = p[:-1]
			if not num:
				raise AwlSimError("Invalid time immediate: "
					"No time amount specified.")
			num = int(num, 10)
			seconds += num * mult
		if isNegative:
			seconds *= -1.0
		return seconds

	@classmethod
	def formatTime(cls, seconds, leadingZeros=False):
		# Format a seconds value into time format.
		d = int(seconds // 86400)
		seconds -= d * 86400
		h = int(seconds // 3600)
		seconds -= h * 3600
		m = int(seconds // 60)
		seconds -= m * 60
		s = int(seconds)
		seconds -= s
		ms = int(seconds * 1000.0)
		ret = []
		for v, b, d in ((d, "d", 1), (h, "h", 2), (m, "m", 2),
				(s, "s", 2), (ms, "ms", 3)):
			if not v:
				continue
			if leadingZeros:
				fmt = "%0" + str(d) + "d%s"
				ret.append(fmt % (v, b))
			else:
				ret.append("%d%s" % (v, b))
		if not ret:
			return "0ms"
		return "".join(ret)

	@classmethod
	def __tryParseImmediate_STRING(cls, token, maxLen=254):
		if not token.startswith("'") or\
		   not token.endswith("'"):
			return None
		token = token[1:-1]
		if len(token) > maxLen:
			raise AwlSimError("String too long (>%d characters)" % maxLen)
		from awlsim.common.sources import AwlSource
		try:
			# Strings are supposed to be traditional
			# "latin1" encoding compatible and the resulting
			# bytes are "latin1" encoded bytes.
			if isMicroPython:
				trans = pivotDict(AwlSource.latin1Trans)
				data = bytearray()
				for c in token:
					if c in trans:
						data += trans[c]
					else:
						data += c.encode(AwlSource.COMPAT_ENCODING)
			else:
				data = token.encode(AwlSource.COMPAT_ENCODING)
			data = bytearray(data)
			if len(data) != len(token):
				raise ValueError
		except (UnicodeError, ValueError) as e:
			raise AwlSimError("Invalid characters in string '%s'. "
				"Please make sure the string is "
				"%s encoding compatible." % (
				token, AwlSource.COMPAT_ENCODING))
		return data

	@classmethod
	def tryParseImmediate_STRING(cls, token):
		data = cls.__tryParseImmediate_STRING(token)
		if data is None:
			return None
		# Add max-length and actual-length bytes.
		data = bytearray((len(data), len(data))) + data
		return data

	@classmethod
	def tryParseImmediate_CHAR(cls, token):
		data = cls.__tryParseImmediate_STRING(token, 1)
		if data is None:
			return None
		# Return it as integer.
		return data[0]

	@classmethod
	def tryParseImmediate_S5T(cls, token):
		token = token.upper()
		if not token.startswith("S5T#"):
			return None
		seconds = cls.__parseGenericTime(token,
						 allowNegative=False)
		s5t = Timer_seconds_to_s5t(seconds)
		return s5t

	@classmethod
	def tryParseImmediate_TIME(cls, token):
		token = token.upper()
		if not token.startswith("T#") and\
		   not token.startswith("TIME#"):
			return None
		seconds = cls.__parseGenericTime(token,
						 allowNegative=True)
		msec = int(seconds * 1000)
		if msec > 0x7FFFFFFF:
			raise AwlSimError("T# time too big")
		return msec

	@classmethod
	def tryParseImmediate_TOD(cls, tokens):
		token = "".join(tokens)
		if not token.startswith("TOD#") and\
		   not token.startswith("TIME_OF_DAY#"):
			return None
		token = token[token.find("#") + 1 : ] # Remove prefix
		try:
			time = token.split(":")
			if len(time) != 3:
				raise ValueError
			hours, minutes, fseconds = int(time[0]), int(time[1]), float(time[2])
			seconds = int(fseconds)
			msecs = int(round(fseconds * 1000.0)) - (seconds * 1000)
			if hours < 0 or hours > 23 or\
			   minutes < 0 or minutes > 59 or\
			   seconds < 0 or seconds > 59 or\
			   msecs < 0 or msecs > 999:
				raise ValueError
			val = hours * 60 * 60 * 1000 +\
			      minutes * 60 * 1000 +\
			      seconds * 1000 +\
			      msecs
			return val
		except ValueError:
			raise AwlSimError("Invalid TIME_OF_DAY immediate")

	@classmethod
	def tryParseImmediate_DATE(cls, token):
		token = token.upper()
		if not token.startswith("D#") and\
		   not token.startswith("DATE#"):
			return None
		token = token[token.find("#") + 1 : ] # Remove prefix
		try:
			date = token.split("-")
			if len(date) != 3:
				raise ValueError
			year, month, day = int(date[0]), int(date[1]), int(date[2])
			delta = datetime.date(year, month, day) -\
				datetime.date(1990, 1, 1)
			days = delta.days
			if days < 0 or days > 65378:
				raise ValueError
			return days
		except ValueError:
			raise AwlSimError("Invalid DATE immediate")

	dateAndTimeWeekdayMap = {
		0	: 2,	# monday
		1	: 3,	# tuesday
		2	: 4,	# wednesday
		3	: 5,	# thursday
		4	: 6,	# friday
		5	: 7,	# saturday
		6	: 1,	# sunday
	}

	@classmethod
	def tryParseImmediate_DT(cls, tokens):
		token = "".join(tokens)
		if not token.startswith("DT#") and\
		   not token.startswith("DATE_AND_TIME#"):
			return None
		token = token[token.find("#") + 1 : ] # Remove prefix
		try:
			idx = token.rfind("-")
			date = token[ : idx]
			time = token[idx + 1 : ]
			date = date.split("-")
			time = time.split(":")
			if len(date) != 3 or len(time) != 3:
				raise ValueError
			year, month, day = int(date[0]), int(date[1]), int(date[2])
			weekday = datetime.date(year, month, day).weekday()
			weekday = cls.dateAndTimeWeekdayMap[weekday]
			if year >= 100:
				year -= 1900
				if year > 99:
					year -= 100
			if year < 0 or year > 99 or\
			   month < 1 or month > 12 or\
			   day < 1 or day > 31:
				raise ValueError
			year = (year % 10) | (((year // 10) % 10) << 4)
			month = (month % 10) | (((month // 10) % 10) << 4)
			day = (day % 10) | (((day // 10) % 10) << 4)
			hour, minute, fsecond = int(time[0]), int(time[1]), float(time[2])
			second = int(fsecond)
			msec = int(fsecond * 1000) - (second * 1000)
			if hour < 0 or hour > 23 or\
			   minute < 0 or minute > 59 or\
			   second < 0 or second > 59 or\
			   msec < 0 or msec > 999:
				raise ValueError
			hour = (hour % 10) | (((hour // 10) % 10) << 4)
			minute = (minute % 10) | (((minute // 10) % 10) << 4)
			second = (second % 10) | (((second // 10) % 10) << 4)
			msec = (msec % 10) | (((msec // 10) % 10) << 4) |\
			       (((msec // 100) % 10) << 8)
			data = bytearray( (year, month, day, hour, minute, second,
					   (msec >> 4) & 0xFF,
					   ((msec & 0xF) << 4) | weekday) )
		except ValueError:
			raise AwlSimError("Invalid DATE_AND_TIME immediate")
		return data

	@classmethod
	def __parsePtrOffset(cls, string):
		try:
			values = string.split(".")
			if len(values) != 2:
				raise ValueError
			byteOffset = int(values[0], 10)
			bitOffset = int(values[1], 10)
			if bitOffset < 0 or bitOffset > 7 or\
			   byteOffset < 0 or byteOffset > 0xFFFF:
				raise ValueError
			return (byteOffset << 3) | bitOffset
		except ValueError:
			raise AwlSimError("Invalid pointer offset: " + string)

	@classmethod
	def tryParseImmediate_Pointer(cls, tokens):
#@cy		cdef Pointer pointer

		prefix = tokens[0]
		if not prefix.upper().startswith("P#"):
			return None, None

		from awlsim.core.memory import Pointer, DBPointer, SymbolicDBPointer

		prefix = prefix[2:] # Strip P#

		pointer = None
		dotIdx = prefix.find(".")
		if dotIdx >= 3 and prefix[:dotIdx].upper().startswith("DB") and\
		   isdecimal(prefix[2:dotIdx]):
			# Parse DB number prefix
			dbNr = int(prefix[2:dotIdx])
			prefix = prefix[dotIdx+1:]
			pointer = DBPointer(0, dbNr)
		elif dotIdx >= 3 and prefix.startswith('"'):
			endIdx = prefix[1:].find('".') + 1
			if endIdx >= 2:
				# Parse symbolic DB name prefix
				dbSymbol = prefix[1:endIdx]
				prefix = prefix[endIdx+2:]
				pointer = SymbolicDBPointer(dbSymbol = dbSymbol)

		if pointer is None:
			# There is no DB number.
			pointer = Pointer(0)

		prefix = prefix.upper()
		try:
			def matchPrefix(prefix, template):
				try:
					return prefix == template or\
					       (prefix.startswith(template) and\
						isdecimal(prefix[len(template)]))
				except IndexError as e:
					return False

			try:
				valueToken = tokens[1]
				nrTokens = 2
			except IndexError as e:
				valueToken = ""
				nrTokens = 1

			if matchPrefix(prefix, "P"):
				ptr = cls.__parsePtrOffset(prefix[1:] + valueToken) |\
					0x80000000
				pointer.setDWord(ptr)
			elif matchPrefix(prefix, "E") or matchPrefix(prefix, "I"):
				ptr = cls.__parsePtrOffset(prefix[1:] + valueToken) |\
					0x81000000
				pointer.setDWord(ptr)
			elif matchPrefix(prefix, "A") or matchPrefix(prefix, "Q"):
				ptr = cls.__parsePtrOffset(prefix[1:] + valueToken) |\
					0x82000000
				pointer.setDWord(ptr)
			elif matchPrefix(prefix, "M"):
				ptr = cls.__parsePtrOffset(prefix[1:] + valueToken) |\
					0x83000000
				pointer.setDWord(ptr)
			elif matchPrefix(prefix, "DBX"):
				ptr = cls.__parsePtrOffset(prefix[3:] + valueToken) |\
					0x84000000
				pointer.setDWord(ptr)
			elif matchPrefix(prefix, "DIX"):
				ptr = cls.__parsePtrOffset(prefix[3:] + valueToken) |\
					0x85000000
				pointer.setDWord(ptr)
			elif matchPrefix(prefix, "L"):
				ptr = cls.__parsePtrOffset(prefix[1:] + valueToken) |\
					0x86000000
				pointer.setDWord(ptr)
			else:
				if isinstance(pointer, DBPointer) and\
				   prefix and not isdecimal(prefix[0]):
					# This is a named DB variable pointer.
					# (awlsim extension).
					if not isinstance(pointer, SymbolicDBPointer):
						pointer = SymbolicDBPointer(pointer.toPointerValue(),
									    pointer.dbNr)
					tokens = [ prefix ] + tokens[1:]
					tokens, nrTokens = AwlDataIdentChain.expandTokens(tokens)
					pointer.identChain = AwlDataIdentChain.parseTokens(tokens)
					return pointer, nrTokens
				# Area-internal pointer
				ptr = cls.__parsePtrOffset(prefix)
				pointer.setDWord(ptr)
				nrTokens = 1
		except IndexError:
			raise AwlSimError("Invalid pointer immediate")

		# Check if this is an ANY pointer immediate.
		if len(tokens) >= nrTokens + 2:
			typeToken = tokens[nrTokens]
			countToken = tokens[nrTokens + 1]

			# Is this a possible ANY pointer?
			# Try to look up the data type name.
			try:
				cls._name2typeid(typeToken)
				isAnyPtr = True
			except AwlSimError as e:
				isAnyPtr = False

			if isAnyPtr:
				# Parse the data type and repetition count.
				dataType = AwlDataType.makeByName(typeToken)
				try:
					count = int(countToken, 10)
					if count < 1 or count > 65535 or\
					   count * (dataType.width // 8) > 65535:
						raise ValueError
				except ValueError as e:
					raise AwlSimError("ANY pointer: "
						"Invalid repetition count.")

				# Convert pointer to ANY
				pointer = pointer.toANYPointer()
				pointer.dataType = dataType
				pointer.count = count

				nrTokens += 2

		return pointer, nrTokens

	@classmethod
	def tryParseImmediate_Bin(cls, token):
		token = token.upper()
		if not token.startswith("2#"):
			return None
		try:
			string = token[2:].replace('_', '')
			immediate = int(string, 2)
			if immediate > 0xFFFFFFFF:
				raise ValueError
		except ValueError as e:
			raise AwlSimError("Invalid immediate")
		return immediate

	@classmethod
	def tryParseImmediate_ByteArray(cls, tokens):
		if tokens[0].upper() != "B#" or tokens[1] != "(":
			return None, None
		try:
			if len(tokens) >= 6 and\
			   tokens[3] == ',' and\
			   tokens[5] == ')':
				fields = 6
				a, b = int(tokens[2], 10),\
				       int(tokens[4], 10)
				if a < 0 or a > 0xFF or\
				   b < 0 or b > 0xFF:
					raise ValueError
				immediate = (a << 8) | b
			elif len(tokens) >= 10 and\
			     tokens[3] == ',' and\
			     tokens[5] == ',' and\
			     tokens[7] == ',' and\
			     tokens[9] == ')':
				fields = 10
				a, b, c, d = int(tokens[2], 10),\
					     int(tokens[4], 10),\
					     int(tokens[6], 10),\
					     int(tokens[8], 10)
				if a < 0 or a > 0xFF or\
				   b < 0 or b > 0xFF or\
				   c < 0 or c > 0xFF or\
				   d < 0 or d > 0xFF:
					raise ValueError
				immediate = (a << 24) | (b << 16) |\
					    (c << 8) | d
			else:
				raise ValueError
		except ValueError as e:
			raise AwlSimError("Invalid immediate")
		return immediate, fields

	@classmethod
	def tryParseImmediate_HexByte(cls, token):
		token = token.upper()
		if not token.startswith("B#16#"):
			return None
		try:
			immediate = int(token[5:], 16)
			if immediate > 0xFF:
				raise ValueError
		except ValueError as e:
			raise AwlSimError("Invalid immediate")
		return immediate

	@classmethod
	def tryParseImmediate_HexWord(cls, token):
		token = token.upper()
		if not token.startswith("W#16#"):
			return None
		try:
			immediate = int(token[5:], 16)
			if immediate > 0xFFFF:
				raise ValueError
		except ValueError as e:
			raise AwlSimError("Invalid immediate")
		return immediate

	@classmethod
	def tryParseImmediate_HexDWord(cls, token):
		token = token.upper()
		if not token.startswith("DW#16#"):
			return None
		try:
			immediate = int(token[6:], 16)
			if immediate > 0xFFFFFFFF:
				raise ValueError
		except ValueError as e:
			raise AwlSimError("Invalid immediate")
		return immediate
