#=============================================================================#
# neksuite                                                                    #
#                                                                             #
# A python module for reading and writing nek5000 files                       #
#                                                                             #
# Authors: Jacopo Canton, Nicolo' Fabbiane, Guillaume Chauvat                 #
# Contacts: jacopo.canton(at)gmail.com                                        #
# Last edit: 2020-02-21                                                       #
#=============================================================================#
import struct
import numpy as np
import pymech.exadata as exdat
import sys

from pymech.log import logger


#==============================================================================
def readnek(fname):
	"""A function for reading binary data from the nek5000 binary format

	Parameters
	----------
	fname : str
		file name
	"""
	#
	try:
		infile = open(fname, 'rb')
	except IOError as e:
		logger.critical('I/O error ({0}): {1}'.format(e.errno, e.strerror))
		return -1
	#
	#---------------------------------------------------------------------------
	# READ HEADER
	#---------------------------------------------------------------------------
	#
	# read header
	header = infile.read(132).split()
	logger.debug('Header: {}'.format(b' '.join(header).decode('utf-8')))

	# get word size: single or double precision
	wdsz = int(header[1])
	if (wdsz == 4):
		realtype = 'f'
	elif (wdsz == 8):
		realtype = 'd'
	else:
		logger.error('Could not interpret real type (wdsz = %i)' % (wdsz))
		return -2
	#
	# get polynomial order
	lr1 = [
		int(header[2]),
		int(header[3]),
		int(header[4])
	]
	#
	# compute total number of points per element
	npel = lr1[0] * lr1[1] * lr1[2]
	#
	# get number of physical dimensions
	ndim = 2 + (lr1[2]>1)
	#
	# get number of elements
	nel = int(header[5])
	#
	# get number of elements in the file
	nelf = int(header[6])
	#
	# get current time
	time = float(header[7])
	#
	# get current time step
	istep = int(header[8])
	#
	# get file id
	fid = int(header[9])
	#
	# get tot number of files
	nf = int(header[10])
	#
	# get variables [XUPTS[01-99]]
	variables = header[11].decode('utf-8')
	logger.debug("Variables: {}".format(variables))
	var = [0 for i in range(5)]
	for v in variables:
		if (v == 'X'):
			var[0] = ndim
		elif (v == 'U'):
			var[1] = ndim
		elif (v == 'P'):
			var[2] = 1
		elif (v == 'T'):
			var[3] = 1
		elif (v == 'S'):
			# For example: variables = 'XS44'
			index_s = variables.index('S')
			nb_scalars = int(variables[index_s+1:])
			var[4] = nb_scalars
	#
	# identify endian encoding
	etagb = infile.read(4)
	etagL = struct.unpack('<f', etagb)[0]; etagL = int(etagL*1e5)/1e5
	etagB = struct.unpack('>f', etagb)[0]; etagB = int(etagB*1e5)/1e5
	if (etagL == 6.54321):
		logger.debug('Reading little-endian file\n')
		emode = '<'
	elif (etagB == 6.54321):
		logger.debug('Reading big-endian file\n')
		emode = '>'
	else:
		logger.error('Could not interpret endianness')
		return -3
	#
	# read element map for the file
	elmap = infile.read(4*nelf)
	elmap = list(struct.unpack(emode+nelf*'i', elmap))
	#
	#---------------------------------------------------------------------------
	# READ DATA
	#---------------------------------------------------------------------------
	#
	# initialize data structure
	data = exdat.exadata(ndim, nel, lr1, var, 0)
	data.time   = time
	data.istep  = istep
	data.wdsz   = wdsz
	data.elmap  = np.array(elmap, dtype=np.int32)
	if (emode == '<'):
		data.endian = 'little'
	elif (emode == '>'):
		data.endian = 'big'
	#
	def read_file_into_data(data_var, index_var):
		"""Read binary file into an array attribute of ``data.elem``"""
		fi = infile.read(npel*wdsz)
		fi = np.frombuffer(fi, dtype=emode+realtype, count=npel)

		# Replace elem array in-place with
		# array read from file after reshaping as
		elem_shape = lr1[::-1]  # lz, ly, lx
		data_var[index_var, ...] = fi.reshape(elem_shape)
	#
	# read geometry
	for iel in elmap:
		el = data.elem[iel-1]
		for idim in range(var[0]): # if var[0] == 0, geometry is not read
			read_file_into_data(el.pos, idim)
	#
	# read velocity
	for iel in elmap:
		el = data.elem[iel-1]
		for idim in range(var[1]): # if var[1] == 0, velocity is not read
			read_file_into_data(el.vel, idim)
	#
	# read pressure
	for iel in elmap:
		el = data.elem[iel-1]
		for ivar in range(var[2]): # if var[2] == 0, pressure is not read
			read_file_into_data(el.pres, ivar)
	#
	# read temperature
	for iel in elmap:
		el = data.elem[iel-1]
		for ivar in range(var[3]): # if var[3] == 0, temperature is not read
			read_file_into_data(el.temp, ivar)
	#
	# read scalar fields
	#
	# NOTE: This is not a bug!
	# Unlike other variables, scalars are in the outer loop and elements
	# are in the inner loop
	#
	for ivar in range(var[4]): # if var[4] == 0, scalars are not read
		for iel in elmap:
			el = data.elem[iel-1]
			read_file_into_data(el.scal, ivar)
	#
	#
	# close file
	infile.close()
	#
	# output
	return data


#==============================================================================
def writenek(fname, data):
	"""A function for writing binary data in the nek5000 binary format

	Parameters
	----------
	fname : str
		file name
	data : exadata
		data organised as readnek() output
	"""
	#
	try:
		outfile = open(fname, 'wb')
	except IOError as e:
		logger.critical('I/O error ({0}): {1}'.format(e.errno, e.strerror))
		return -1
	#
	#---------------------------------------------------------------------------
	# WRITE HEADER
	#---------------------------------------------------------------------------
	#
	# multiple files (not implemented)
	fid = 0
	nf = 1
	nelf = data.nel
	#
	# get fields to be written
	vars = ''
	if (data.var[0] > 0): vars += 'X'
	if (data.var[1] > 0): vars += 'U'
	if (data.var[2] > 0): vars += 'P'
	if (data.var[3] > 0): vars += 'T'
	if (data.var[4] > 0):
		# TODO: check if header for scalars are written with zeros filled as S01
		vars += 'S{:02d}'.format(data.var[4])
	#
	# get word size
	if (data.wdsz == 4):
		logger.debug('Writing single-precision file')
	elif (data.wdsz == 8):
		logger.debug('Writing double-precision file')
	else:
		logger.error('Could not interpret real type (wdsz = %i)' % (data.wdsz))
		return -2
	#
	# generate header
	header = '#std %1i %2i %2i %2i %10i %10i %20.13E %9i %6i %6i %s' %(
		data.wdsz, data.lr1[0], data.lr1[1], data.lr1[2], data.nel, nelf,
		data.time, data.istep, fid, nf, vars)
	#
	# write header
	header = header.ljust(132)
	outfile.write(header.encode('utf-8'))
	#
	# decide endianness
	if data.endian in ('big', 'little'):
		byteswap = data.endian != sys.byteorder
		logger.debug(f'Writing {data.endian}-endian file')
	else:
		byteswap = False
		logger.warning(
			f'Unrecognized endianness {data.endian}, '
			f'writing native {sys.byteorder}-endian file'
		)
	#
	def correct_endianness(a):
		''' Return the array with the requested endianness'''
		if byteswap:
			return a.byteswap()
		else:
			return a
	#
	# write tag (to specify endianness)
	endianbytes = np.array([6.54321], dtype=np.float32)
	correct_endianness(endianbytes).tofile(outfile)
	#
	# write element map for the file
	correct_endianness(data.elmap).tofile(outfile)
	#
	#---------------------------------------------------------------------------
	# WRITE DATA
	#---------------------------------------------------------------------------
	#
	# compute total number of points per element
	npel = data.lr1[0] * data.lr1[1] * data.lr1[2]
	#
	def write_ndarray_to_file(a):
		'''Write a data array to the output file in the requested precision and endianness'''
		if data.wdsz == 4:
			correct_endianness(a.astype(np.float32)).tofile(outfile)
		else:
			correct_endianness(a).tofile(outfile)
	#
	# write geometry
	for iel in data.elmap:
		for idim in range(data.var[0]): # if var[0] == 0, geometry is not written
			write_ndarray_to_file(data.elem[iel-1].pos[idim, :, :, :])
	#
	# write velocity
	for iel in data.elmap:
		for idim in range(data.var[1]): # if var[1] == 0, velocity is not written
			write_ndarray_to_file(data.elem[iel-1].vel[idim, :, :, :])
	#
	# write pressure
	for iel in data.elmap:
		for ivar in range(data.var[2]): # if var[2] == 0, pressure is not written
			write_ndarray_to_file(data.elem[iel-1].pres[ivar, :, :, :])
	#
	# write temperature
	for iel in data.elmap:
		for ivar in range(data.var[3]): # if var[3] == 0, temperature is not written
			write_ndarray_to_file(data.elem[iel-1].temp[ivar, :, :, :])
	#
	# write scalars
	#
	# NOTE: This is not a bug!
	# Unlike other variables, scalars are in the outer loop and elements
	# are in the inner loop
	#
	for ivar in range(data.var[4]): # if var[4] == 0, scalars are not written
		for iel in data.elmap:
			write_ndarray_to_file(data.elem[iel-1].scal[ivar, :, :, :])
	#
	# write max and min of every field in every element (forced to single precision)
	if (data.ndim==3):
		#
		for iel in data.elmap:
			for idim in range(data.var[0]):
				correct_endianness(np.min(data.elem[iel-1].pos[idim, :,:,:]).astype(np.float32)).tofile(outfile)
				correct_endianness(np.max(data.elem[iel-1].pos[idim, :,:,:]).astype(np.float32)).tofile(outfile)
		for iel in data.elmap:
			for idim in range(data.var[1]):
				correct_endianness(np.min(data.elem[iel-1].vel[idim, :,:,:]).astype(np.float32)).tofile(outfile)
				correct_endianness(np.max(data.elem[iel-1].vel[idim, :,:,:]).astype(np.float32)).tofile(outfile)
		for iel in data.elmap:
			for ivar in range(data.var[2]):
				correct_endianness(np.min(data.elem[iel-1].pres[ivar, :,:,:]).astype(np.float32)).tofile(outfile)
				correct_endianness(np.max(data.elem[iel-1].pres[ivar, :,:,:]).astype(np.float32)).tofile(outfile)
		for iel in data.elmap:
			for ivar in range(data.var[3]):
				correct_endianness(np.min(data.elem[iel-1].temp[ivar, :,:,:]).astype(np.float32)).tofile(outfile)
				correct_endianness(np.max(data.elem[iel-1].temp[ivar, :,:,:]).astype(np.float32)).tofile(outfile)
		for iel in data.elmap:
			for ivar in range(data.var[4]):
				correct_endianness(np.min(data.elem[iel-1].scal[ivar, :,:,:]).astype(np.float32)).tofile(outfile)
				correct_endianness(np.max(data.elem[iel-1].scal[ivar, :,:,:]).astype(np.float32)).tofile(outfile)

	# close file
	outfile.close()
	#
	# output
	return 0


#==============================================================================
def readrea(fname):
	"""A function for reading .rea files for nek5000

	Parameters
	----------
	fname : str
		file name
	"""
	#
	try:
		infile = open(fname, 'r')
	except IOError as e:
		logger.critical('I/O error ({0}): {1}'.format(e.errno, e.strerror))
		#return -1
	#
	#---------------------------------------------------------------------------
	# count the number of boundary conditions
	# (it's too dangerous to infer it from the header)
	#---------------------------------------------------------------------------
	#
	nbc = 0
	for line in infile:
		line_split = line.split()
		if 'BOUNDARY' in line_split[2:] and not 'NO' in line_split:
			nbc = nbc + 1

	infile.seek(0)
	#
	#---------------------------------------------------------------------------
	# READ HEADER (2 lines) + ndim + number of parameters
	#---------------------------------------------------------------------------
	#
	infile.readline()
	infile.readline()
	ndim = int(infile.readline().split()[0])
	npar = int(infile.readline().split()[0])
	#
	nface = 2*ndim
	#
	#---------------------------------------------------------------------------
	# READ parameters
	#---------------------------------------------------------------------------
	#
	param = np.zeros((npar,1))
	for ipar in range(npar):
		param[ipar] = float(infile.readline().split()[0])
	#
	#---------------------------------------------------------------------------
	# skip passive scalars
	#---------------------------------------------------------------------------
	#
	npscal_data = int(infile.readline().split()[0])
	for ipscal in range(npscal_data):
		infile.readline()
	#
	#---------------------------------------------------------------------------
	# skip logical switches
	#---------------------------------------------------------------------------
	#
	nswitch = int(infile.readline().split()[0])
	for iswitch in range(nswitch):
		infile.readline()
	#
	#---------------------------------------------------------------------------
	# skip XFAC,YFAC,XZERO,YZERO
	#---------------------------------------------------------------------------
	#
	infile.readline()
	#
	#---------------------------------------------------------------------------
	# READ MESH
	#---------------------------------------------------------------------------
	#
	infile.readline()
	nel = int(infile.readline().split()[0])
	#
	# initialize data structure
	lr1 = [2, 2, ndim-1]
	var = [ndim, 0, 0, 0, 0]
	#
	data = exdat.exadata(ndim, nel, lr1, var, nbc)
	#
	# read geometry
	for iel in range(nel):
		# skip element number and group
		infile.readline()
		for idim in range(var[0]-1): # if ndim == 3 do this twice
			for jdim in range(var[0]):
				fi = infile.readline().split()
				data.elem[iel].pos[jdim,idim,0,0] = float(fi[0])
				data.elem[iel].pos[jdim,idim,0,1] = float(fi[1])
				data.elem[iel].pos[jdim,idim,1,1] = float(fi[2])
				data.elem[iel].pos[jdim,idim,1,0] = float(fi[3])
	#
	#---------------------------------------------------------------------------
	# CURVED SIDE DATA
	#---------------------------------------------------------------------------
	#
	infile.readline()
	ncurved = int(infile.readline().split()[0])
	data.ncurv = ncurved
	for icurved in range(ncurved):
		line = infile.readline()
		if (nel < 1e3):
			iedge = int(line[0:3])-1
			iel   = int(line[3:6])-1
			data.elem[iel].curv[iedge][0] = float(line[6:20])
			data.elem[iel].curv[iedge][1] = float(line[20:34])
			data.elem[iel].curv[iedge][2] = float(line[34:48])
			data.elem[iel].curv[iedge][3] = float(line[48:62])
			data.elem[iel].curv[iedge][4] = float(line[62:76])
			data.elem[iel].ccurv[iedge]   = line[76:79].split()[0]
		elif (nel < 1e6):
			iedge = int(line[0:2])-1
			iel   = int(line[2:8])-1
			data.elem[iel].curv[iedge][0] = float(line[8:22])
			data.elem[iel].curv[iedge][1] = float(line[22:36])
			data.elem[iel].curv[iedge][2] = float(line[36:50])
			data.elem[iel].curv[iedge][3] = float(line[50:64])
			data.elem[iel].curv[iedge][4] = float(line[64:78])
			data.elem[iel].ccurv[iedge]   = line[78:81].split()[0]
		else:
			iedge = int(line[0:2])-1
			iel   = int(line[2:12])-1
			data.elem[iel].curv[iedge][0] = float(line[12:26])
			data.elem[iel].curv[iedge][1] = float(line[26:40])
			data.elem[iel].curv[iedge][2] = float(line[40:54])
			data.elem[iel].curv[iedge][3] = float(line[54:68])
			data.elem[iel].curv[iedge][4] = float(line[68:82])
			data.elem[iel].ccurv[iedge]   = line[82:85].split()[0]
	#
	#---------------------------------------------------------------------------
	# BOUNDARY CONDITIONS
	#---------------------------------------------------------------------------
	#
	infile.readline()  # ***** BOUNDARY CONDITIONS *****
	for ibc in range(nbc):
		infile.readline()  # ***** FLUID   BOUNDARY CONDITIONS ***** [or similar]
		for iel in range(nel):
			for iface in range(nface):
				line = infile.readline()
				if (nel < 1e3):
					data.elem[iel].bcs[ibc, iface][0] = line[1:3].strip()
					data.elem[iel].bcs[ibc, iface][1] = int(line[4:7])
					data.elem[iel].bcs[ibc, iface][2] = int(line[7:10])
					data.elem[iel].bcs[ibc, iface][3] = float(line[10:24])
					data.elem[iel].bcs[ibc, iface][4] = float(line[24:38])
					data.elem[iel].bcs[ibc, iface][5] = float(line[38:52])
					data.elem[iel].bcs[ibc, iface][6] = float(line[52:66])
					data.elem[iel].bcs[ibc, iface][7] = float(line[66:80])
				elif (nel < 1e6):
					data.elem[iel].bcs[ibc, iface][0] = line[1:3].strip()
					data.elem[iel].bcs[ibc, iface][1] = iel + 1
					data.elem[iel].bcs[ibc, iface][2] = iface + 1
					data.elem[iel].bcs[ibc, iface][3] = float(line[10:24])
					data.elem[iel].bcs[ibc, iface][4] = float(line[24:38])
					data.elem[iel].bcs[ibc, iface][5] = float(line[38:52])
					data.elem[iel].bcs[ibc, iface][6] = float(line[52:66])
					data.elem[iel].bcs[ibc, iface][7] = float(line[66:80])
				else:
					data.elem[iel].bcs[ibc, iface][0] = line[1:3].strip()
					data.elem[iel].bcs[ibc, iface][1] = int(line[4:15])
					data.elem[iel].bcs[ibc, iface][2] = int(line[15:16])
					data.elem[iel].bcs[ibc, iface][3] = float(line[16:34])
					data.elem[iel].bcs[ibc, iface][4] = float(line[34:52])
					data.elem[iel].bcs[ibc, iface][5] = float(line[52:70])
					data.elem[iel].bcs[ibc, iface][6] = float(line[70:88])
					data.elem[iel].bcs[ibc, iface][7] = float(line[88:106])
				# ignore some invalid internal 'E' conditions.
				# They are typically written this way by re2torea and Nek5000 ignores them.
				if data.elem[iel].bcs[ibc, iface][0] == 'E' and data.elem[iel].bcs[ibc, iface][3] == 0.:
					data.elem[iel].bcs[ibc, iface][0] = ''
					for j in range(1, 8):
						data.elem[iel].bcs[ibc, iface][j] = 0
		ibc = ibc + 1
	#
	#---------------------------------------------------------------------------
	# FORGET ABOUT WHAT FOLLOWS
	#---------------------------------------------------------------------------
	#
	#
	# close file
	infile.close()
	#
	# output
	return data


#==============================================================================
def writerea(fname, data):
	"""A function for writing ascii .rea files for nek5000

	Parameters
	----------
	fname : str
		file name
	data : exadata
		data organised as in exadata.py
	"""
	#
	try:
		outfile = open(fname, 'w')
	except IOError as e:
		logger.critical('I/O error ({0}): {1}'.format(e.errno, e.strerror))
		#return -1
	#
	#---------------------------------------------------------------------------
	# READ HEADER (2 lines) + ndim + number of parameters
	#---------------------------------------------------------------------------
	#
	outfile.write('****** PARAMETERS ******\n')
	outfile.write('   2.6000     NEKTON VERSION\n')
	outfile.write('   {0:1d} DIMENSIONAL RUN\n'.format(data.ndim))
	outfile.write('         118 PARAMETERS FOLLOW\n')
	outfile.write('   1.00000     P001: DENSITY\n')
	outfile.write('  -1000.00     P002: VISCOSITY\n')
	outfile.write('   0.00000     P003: BETAG\n')
	outfile.write('   0.00000     P004: GTHETA\n')
	outfile.write('   0.00000     P005: PGRADX\n')
	outfile.write('   0.00000     P006: \n')
	outfile.write('   1.00000     P007: RHOCP\n')
	outfile.write('   1.00000     P008: CONDUCT\n')
	outfile.write('   0.00000     P009: \n')
	outfile.write('   0.00000     P010: FINTIME\n')
	outfile.write('   103         P011: NSTEPS\n')
	outfile.write('  -1.00000E-03 P012: DT\n')
	outfile.write('   0.00000     P013: IOCOMM\n')
	outfile.write('   0.00000     P014: IOTIME\n')
	outfile.write('   10          P015: IOSTEP\n')
	outfile.write('   0.00000     P016: PSSOLVER: 0=default\n')
	outfile.write('   1.00000     P017: \n')
	outfile.write('   0.00000     P018: GRID <0 --> # cells on screen\n')
	outfile.write('   0.00000     P019: INTYPE\n')
	outfile.write('   10.0000     P020: NORDER\n')
	outfile.write('   1.00000E-09 P021: DIVERGENCE\n')
	outfile.write('   1.00000E-09 P022: HELMHOLTZ\n')
	outfile.write('   0.00000     P023: NPSCAL\n')
	outfile.write('   1.00000E-02 P024: TOLREL\n')
	outfile.write('   1.00000E-02 P025: TOLABS\n')
	outfile.write('   1.00000     P026: COURANT/NTAU\n')
	outfile.write('   3.00000     P027: TORDER\n')
	outfile.write('   0.00000     P028: TORDER: mesh velocity (0: p28=p27)\n')
	outfile.write('   0.00000     P029: = magnetic visc if > 0, = -1/Rm if < 0\n')
	outfile.write('   0.00000     P030: > 0 ==> properties set in uservp()\n')
	outfile.write('   0.00000     P031: NPERT: #perturbation modes\n')
	outfile.write('   0.00000     P032: #BCs in re2 file, if > 0\n')
	outfile.write('   0.00000     P033: \n')
	outfile.write('   0.00000     P034: \n')
	outfile.write('   0.00000     P035: \n')
	outfile.write('   0.00000     P036: XMAGNET\n')
	outfile.write('   0.00000     P037: NGRIDS\n')
	outfile.write('   0.00000     P038: NORDER2\n')
	outfile.write('   0.00000     P039: NORDER3\n')
	outfile.write('   0.00000     P040: \n')
	outfile.write('   0.00000     P041: 1-->multiplicattive SEMG\n')
	outfile.write('   0.00000     P042: 0=gmres/1=pcg\n')
	outfile.write('   0.00000     P043: 0=semg/1=schwarz\n')
	outfile.write('   0.00000     P044: 0=E-based/1=A-based prec.\n')
	outfile.write('   0.00000     P045: Relaxation factor for DTFS\n')
	outfile.write('   0.00000     P046: reserved\n')
	outfile.write('   0.00000     P047: vnu: mesh material prop.\n')
	outfile.write('   0.00000     P048: \n')
	outfile.write('   0.00000     P049: \n')
	outfile.write('   0.00000     P050: \n')
	outfile.write('   0.00000     P051: \n')
	outfile.write('   0.00000     P052: IOHIS\n')
	outfile.write('   0.00000     P053: \n')
	outfile.write('   0.00000     P054: fixed flow rate dir: |p54|=1,2,3=x,y,z\n')
	outfile.write('   0.00000     P055: vol.flow rate (p54>0) or Ubar (p54<0)\n')
	outfile.write('   0.00000     P056: \n')
	outfile.write('   0.00000     P057: \n')
	outfile.write('   0.00000     P058: \n')
	outfile.write('   0.00000     P059: !=0 --> full Jac. eval. for each el.\n')
	outfile.write('   0.00000     P060: !=0 --> init. velocity to small nonzero\n')
	outfile.write('   0.00000     P061: \n')
	outfile.write('   0.00000     P062: >0 --> force byte_swap for output\n')
	outfile.write('   8.00000     P063: =8 --> force 8-byte output\n')
	outfile.write('   0.00000     P064: =1 --> perturbation restart\n')
	outfile.write('   1.00000     P065: #iofiles (eg, 0 or 64); <0 --> sep. dirs\n')
	outfile.write('   6.00000     P066: output : <0=ascii, else binary\n')
	outfile.write('   6.00000     P067: restart: <0=ascii, else binary\n')
	outfile.write('   0.00000     P068: iastep: freq for avg_all (0=iostep)\n')
	outfile.write('   0.00000     P069:       : freq of srf dump\n')
	outfile.write('   0.00000     P070: \n')
	outfile.write('   0.00000     P071: \n')
	outfile.write('   0.00000     P072: \n')
	outfile.write('   0.00000     P073: \n')
	outfile.write('   0.00000     P074: \n')
	outfile.write('   0.00000     P075: \n')
	outfile.write('   0.00000     P076: \n')
	outfile.write('   0.00000     P077: \n')
	outfile.write('   0.00000     P078: \n')
	outfile.write('   0.00000     P079: \n')
	outfile.write('   0.00000     P080: \n')
	outfile.write('   0.00000     P081: \n')
	outfile.write('   0.00000     P082: \n')
	outfile.write('   0.00000     P083: \n')
	outfile.write('   0.00000     P084: != 0 --> sets initial timestep if p12>0\n')
	outfile.write('   0.00000     P085: dt retio of p84 !=0, for timesteps>0\n')
	outfile.write('   0.00000     P086: reserved\n')
	outfile.write('   0.00000     P087: \n')
	outfile.write('   0.00000     P088: \n')
	outfile.write('   0.00000     P089: \n')
	outfile.write('   0.00000     P090: \n')
	outfile.write('   0.00000     P091: \n')
	outfile.write('   0.00000     P092: \n')
	outfile.write('   20.0000     P093: Number of previous pressure solns saved\n')
	outfile.write('   9.00000     P094: start projecting velocity after p94 step\n')
	outfile.write('   9.00000     P095: start projecting pressure after p95 step\n')
	outfile.write('   0.00000     P096: \n')
	outfile.write('   0.00000     P097: \n')
	outfile.write('   0.00000     P098: \n')
	outfile.write('   3.00000     P099: dealiasing: <0--> off /3--> old /4-->new\n')
	outfile.write('   0.00000     P100: \n')
	outfile.write('   0.00000     P101: Number of additional modes to filter\n')
	outfile.write('   0.00000     P102: Dump out divergence at each time step\n')
	outfile.write('   0.01000     P103: weight of stabilizing filter\n')
	outfile.write('   0.00000     P104: \n')
	outfile.write('   0.00000     P105: \n')
	outfile.write('   0.00000     P106: \n')
	outfile.write('   0.00000     P107: !=0 --> add h2 array in hmholtz eqn\n')
	outfile.write('   0.00000     P108: \n')
	outfile.write('   0.00000     P109: \n')
	outfile.write('   0.00000     P110: \n')
	outfile.write('   0.00000     P111: \n')
	outfile.write('   0.00000     P112: \n')
	outfile.write('   0.00000     P113: \n')
	outfile.write('   0.00000     P114: \n')
	outfile.write('   0.00000     P115: \n')
	outfile.write('   0.00000     P116: \n')
	outfile.write('   0.00000     P117: \n')
	outfile.write('   0.00000     P118: \n')
	outfile.write('      4  Lines of passive scalar data follows2 CONDUCT, 2RHOCP\n')
	outfile.write('   1.00000        1.00000        1.00000        1.00000        1.00000\n')
	outfile.write('   1.00000        1.00000        1.00000        1.00000\n')
	outfile.write('   1.00000        1.00000        1.00000        1.00000        1.00000\n')
	outfile.write('   1.00000        1.00000        1.00000        1.00000\n')
	outfile.write('         13   LOGICAL SWITCHES FOLLOW\n')
	outfile.write(' T      IFFLOW\n')
	outfile.write(' F      IFHEAT\n')
	outfile.write(' T      IFTRAN\n')
	outfile.write(' T F F F F F F F F F F  IFNAV & IFADVC (convection in P.S. fields)\n')
	outfile.write(' F F T T T T T T T T T T  IFTMSH (IF mesh for this field is T mesh)\n')
	outfile.write(' F      IFAXIS\n')
	outfile.write(' F      IFSTRS\n')
	outfile.write(' F      IFSPLIT\n')
	outfile.write(' F      IFMGRID\n')
	outfile.write(' F      IFMODEL\n')
	outfile.write(' F      IFKEPS\n')
	outfile.write(' F      IFMVBD\n')
	outfile.write(' F      IFCHAR\n')
	outfile.write('   2.00000       2.00000      -1.00000      -1.00000     XFAC,YFAC,XZERO,YZERO\n')
	#
	# vertex data
	outfile.write('  ***** MESH DATA *****  6 lines are X,Y,Z;X,Y,Z. Columns corners 1-4;5-8\n')
	outfile.write('  {0:10d} {1:1d} {2:10d} NEL,NDIM,NELV\n'.format(data.nel, data.ndim, data.nel))
	for iel in range(data.nel):
		outfile.write('           ELEMENT {0:10d} [  1a]    GROUP 1\n'.format(iel+1))
		if (data.ndim == 2):
			outfile.write('{0:14.6e}{1:14.6e}{2:14.6e}{3:14.6e}\n'.format(data.elem[iel].pos[0, 0, 0, 0], data.elem[iel].pos[0, 0, 0, -1], data.elem[iel].pos[0, 0, -1, -1], data.elem[iel].pos[0, 0, -1, 0]))
			outfile.write('{0:14.6e}{1:14.6e}{2:14.6e}{3:14.6e}\n'.format(data.elem[iel].pos[1, 0, 0, 0], data.elem[iel].pos[1, 0, 0, -1], data.elem[iel].pos[1, 0, -1, -1], data.elem[iel].pos[1, 0, -1, 0]))
		elif (data.ndim == 3):
			outfile.write('{0:14.6e}{1:14.6e}{2:14.6e}{3:14.6e}\n'.format(data.elem[iel].pos[0, 0, 0, 0], data.elem[iel].pos[0, 0, 0, -1], data.elem[iel].pos[0, 0, -1, -1], data.elem[iel].pos[0, 0, -1, 0]))
			outfile.write('{0:14.6e}{1:14.6e}{2:14.6e}{3:14.6e}\n'.format(data.elem[iel].pos[1, 0, 0, 0], data.elem[iel].pos[1, 0, 0, -1], data.elem[iel].pos[1, 0, -1, -1], data.elem[iel].pos[1, 0, -1, 0]))
			outfile.write('{0:14.6e}{1:14.6e}{2:14.6e}{3:14.6e}\n'.format(data.elem[iel].pos[2, 0, 0, 0], data.elem[iel].pos[2, 0, 0, -1], data.elem[iel].pos[2, 0, -1, -1], data.elem[iel].pos[2, 0, -1, 0]))
			#
			outfile.write('{0:14.6e}{1:14.6e}{2:14.6e}{3:14.6e}\n'.format(data.elem[iel].pos[0, -1, 0, 0], data.elem[iel].pos[0, -1, 0, -1], data.elem[iel].pos[0, -1, -1, -1], data.elem[iel].pos[0, -1, -1, 0]))
			outfile.write('{0:14.6e}{1:14.6e}{2:14.6e}{3:14.6e}\n'.format(data.elem[iel].pos[1, -1, 0, 0], data.elem[iel].pos[1, -1, 0, -1], data.elem[iel].pos[1, -1, -1, -1], data.elem[iel].pos[1, -1, -1, 0]))
			outfile.write('{0:14.6e}{1:14.6e}{2:14.6e}{3:14.6e}\n'.format(data.elem[iel].pos[2, -1, 0, 0], data.elem[iel].pos[2, -1, 0, -1], data.elem[iel].pos[2, -1, -1, -1], data.elem[iel].pos[2, -1, -1, 0]))
	#
	# curved side data
	outfile.write('  ***** CURVED SIDE DATA *****\n')
	outfile.write('  {0:10d} Curved sides follow IEDGE,IEL,CURVE(I),I=1,5, CCURVE\n'.format(data.ncurv))
	for iel in range(data.nel):
		if (data.nel < 1e3):
			#
			for iedge in range(12):
				if (data.elem[iel].ccurv[iedge] != ''):
					outfile.write('{0:3d}{1:3d}{2:14.6e}{3:14.6e}{4:14.6e}{5:14.6e}{6:14.6e}{7:>2s}\n'.format(
						iedge+1, iel+1, data.elem[iel].curv[iedge][0], data.elem[iel].curv[iedge][1], data.elem[iel].curv[iedge][2], data.elem[iel].curv[iedge][3], data.elem[iel].curv[iedge][4], data.elem[iel].ccurv[iedge]))
		elif (data.nel < 1e6):
			#
			for iedge in range(12):
				if (data.elem[iel].ccurv[iedge] != ''):
					outfile.write('{0:2d}{1:6d}{2:14.6e}{3:14.6e}{4:14.6e}{5:14.6e}{6:14.6e}{7:>2s}\n'.format(
						iedge+1, iel+1, data.elem[iel].curv[iedge][0], data.elem[iel].curv[iedge][1], data.elem[iel].curv[iedge][2], data.elem[iel].curv[iedge][3], data.elem[iel].curv[iedge][4], data.elem[iel].ccurv[iedge]))
		else:
			#
			for iedge in range(12):
				if (data.elem[iel].ccurv[iedge] != ''):
					outfile.write('{0:2d}{1:10d}{2:14.6e}{3:14.6e}{4:14.6e}{5:14.6e}{6:14.6e}{7:>2s}\n'.format(
						iedge+1, iel+1, data.elem[iel].curv[iedge][0], data.elem[iel].curv[iedge][1], data.elem[iel].curv[iedge][2], data.elem[iel].curv[iedge][3], data.elem[iel].curv[iedge][4], data.elem[iel].ccurv[iedge]))
	#
	# boundary conditions data
	outfile.write('  ***** BOUNDARY CONDITIONS *****\n')
	outfile.write('  ***** FLUID BOUNDARY CONDITIONS *****\n')
	for ibc in range(data.nbc):
		if ibc == 1:
			outfile.write('  ***** THERMAL BOUNDARY CONDITIONS *****\n')
		elif ibc > 1:
			outfile.write('  ***** PASSIVE SCALAR {:4d} BOUNDARY CONDITIONS *****\n'.format(ibc-1))
		for iel in range(data.nel):
			for iface in range(2*data.ndim):
				# if no boundary condition is specified, write 'E' iel iface [...]
				# this is the behaviour of re2torea.
				if data.elem[iel].bcs[ibc, iface][0] == '':
					data.elem[iel].bcs[ibc, iface][0] = 'E'
					data.elem[iel].bcs[ibc, iface][1] = iel+1
					data.elem[iel].bcs[ibc, iface][2] = iface+1
				if (data.nel < 1e3):
					outfile.write(' {0:2s} {1:3d}{2:3d}{3:14.6e}{4:14.6e}{5:14.6e}{6:14.6e}{7:14.6e}\n'.format(
						data.elem[iel].bcs[ibc, iface][0], data.elem[iel].bcs[ibc, iface][1], data.elem[iel].bcs[ibc, iface][2], data.elem[iel].bcs[ibc, iface][3], data.elem[iel].bcs[ibc, iface][4], data.elem[iel].bcs[ibc, iface][5], data.elem[iel].bcs[ibc, iface][6], data.elem[iel].bcs[ibc, iface][7]))
				elif (data.nel < 1e6):
					outfile.write(' {0:2s} {1:6d}{2:14.6e}{3:14.6e}{4:14.6e}{5:14.6e}{6:14.6e}\n'.format(
						data.elem[iel].bcs[ibc, iface][0], data.elem[iel].bcs[ibc, iface][1], data.elem[iel].bcs[ibc, iface][3], data.elem[iel].bcs[ibc, iface][4], data.elem[iel].bcs[ibc, iface][5], data.elem[iel].bcs[ibc, iface][6], data.elem[iel].bcs[ibc, iface][7]))
				else:
					outfile.write(' {0:2s} {1:11d}{2:1d}{3:18.11e}{4:18.11e}{5:18.11e}{6:18.11e}{7:18.11e}\n'.format(
						data.elem[iel].bcs[ibc, iface][0], data.elem[iel].bcs[ibc, iface][1], data.elem[iel].bcs[ibc, iface][2], data.elem[iel].bcs[ibc, iface][3], data.elem[iel].bcs[ibc, iface][4], data.elem[iel].bcs[ibc, iface][5], data.elem[iel].bcs[ibc, iface][6], data.elem[iel].bcs[ibc, iface][7]))

	if data.nbc < 2:
		outfile.write('  ***** NO THERMAL BOUNDARY CONDITIONS *****\n')
	outfile.write('    0 PRESOLVE/RESTART OPTIONS  *****\n')
	outfile.write('    7         INITIAL CONDITIONS *****\n')
	outfile.write(' C Default\n')
	outfile.write(' C Default\n')
	outfile.write(' C Default\n')
	outfile.write(' C Default\n')
	outfile.write(' C Default\n')
	outfile.write(' C Default\n')
	outfile.write(' C Default\n')
	outfile.write('  ***** DRIVE FORCE DATA ***** BODY FORCE, FLOW, Q\n')
	outfile.write('    4                 Lines of Drive force data follow\n')
	outfile.write(' C\n')
	outfile.write(' C\n')
	outfile.write(' C\n')
	outfile.write(' C\n')
	outfile.write(' ***** Variable Property Data ***** Overrrides Parameter data.\n')
	outfile.write('    1 Lines follow.\n')
	outfile.write('    0 PACKETS OF DATA FOLLOW\n')
	outfile.write(' ***** HISTORY AND INTEGRAL DATA *****\n')
	outfile.write('    0   POINTS.  Hcode, I,J,H,IEL\n')
	outfile.write(' ***** OUTPUT FIELD SPECIFICATION *****\n')
	outfile.write('    6 SPECIFICATIONS FOLLOW\n')
	outfile.write('    F      COORDINATES\n')
	outfile.write('    T      VELOCITY\n')
	outfile.write('    T      PRESSURE\n')
	outfile.write('    F      TEMPERATURE\n')
	outfile.write('    F      TEMPERATURE GRADIENT\n')
	outfile.write('    0      PASSIVE SCALARS\n')
	outfile.write(' ***** OBJECT SPECIFICATION *****\n')
	outfile.write('        0 Surface Objects\n')
	outfile.write('        0 Volume  Objects\n')
	outfile.write('        0 Edge    Objects\n')
	outfile.write('        0 Point   Objects\n')
	#
	# close file
	outfile.close()
	#
	# output
	return 0


def readre2(fname):
	"""A function for reading .re2 files for nek5000

	Parameters
	----------
	fname : str
		file name
	"""
	#
	try:
		infile = open(fname, 'rb')
	except IOError as e:
		logger.critical('I/O error ({0}): {1}'.format(e.errno, e.strerror))
		return -1
	# the header for re2 files is 80 ASCII bytes, something like
	# #v002    18669  2    18669 this is the hdr                                      %
	header = infile.read(80).split()
	nel = int(header[1])
	ndim = int(header[2])
	# always double precision
	wdsz = 8
	realtype = 'd'

	# detect endianness
	etagb = infile.read(4)
	etagL = struct.unpack('<f', etagb)[0]; etagL = int(etagL*1e5)/1e5
	etagB = struct.unpack('>f', etagb)[0]; etagB = int(etagB*1e5)/1e5
	if (etagL == 6.54321):
		logger.debug('Reading little-endian file\n')
		emode = '<'
		endian = 'little'
	elif (etagB == 6.54321):
		logger.debug('Reading big-endian file\n')
		emode = '>'
		endian = 'big'
	else:
		logger.error('Could not interpret endianness')
		return -3
	#
	# there are no GLL points here, only quad/hex vertices
	lr1 = [2, 2, ndim-1]
	npel = 2**ndim
	# the file only contains geometry
	var = [ndim, 0, 0, 0, 0]
	# allocate structure
	data = exdat.exadata(ndim, nel, lr1, var, 1)
	#
	# some metadata
	data.wdsz = wdsz
	data.endian = endian
	#
	# read the whole geometry into a buffer
	# for some reason each element is prefixed with 8 bytes of zeros, it's not clear to me why.
	# This is the reason for the +1 here, then the first number is ignored.
	buf = infile.read((ndim*npel+1)*wdsz*nel)
	elem_shape = [ndim, ndim-1, 2, 2]  # nvar, lz, ly, lx
	for (iel, el) in enumerate(data.elem):
		fi = np.frombuffer(buf, dtype=emode+realtype, count=ndim*npel+1, offset=(ndim*npel+1)*wdsz*iel)
		# the data is stored in the following order (2D|3D):
		# x1, x2, x4, x3; | x5, x6, x8, x7;
		# y1, y2, y4, y3; | y5, y6, y8, y7;
		# ----------------
		# z1, z2, z4, z3; z5, z6, z8, z7;
		# where 1-8 is the ordering of the points in memory
		for idim in range(ndim):  # x, y, [z]
			for iz in range(ndim-1):  # this does only one iteration in 2D, and in 3D does one iteration for 1-4 and one for 5-8
				el.pos[idim, iz, 0, 0] = fi[npel*idim+4*iz+1]
				el.pos[idim, iz, 0, 1] = fi[npel*idim+4*iz+2]
				el.pos[idim, iz, 1, 1] = fi[npel*idim+4*iz+3]
				el.pos[idim, iz, 1, 0] = fi[npel*idim+4*iz+4]
	#
	# read curved sides
	# the number of curved sides is stored as a double,
	# then each curved side is 64 bytes:
	# iel iface p1 p2 p3 p4 p5 ctype
	# where p1-5 are f64 parameters and ctype is the type of curvature in ASCII
	ncparam = 8
	buf = infile.read(wdsz)
	ncurv = int(np.frombuffer(buf)[0])
	logger.debug('Found {} curved sides'.format(ncurv))
	data.ncurv = ncurv
	buf = infile.read(wdsz*(ncparam*ncurv))
	for icurv in range(ncurv):
		# interpret the data
		curv = np.frombuffer(buf, dtype=emode+realtype, count=ncparam, offset=icurv*ncparam*wdsz)
		iel = int(curv[0])-1
		iedge = int(curv[1])-1
		cparams = curv[2:7]
		# select only the first byte, because it turns out the later bytes may contain garbage.
		# typically, it's b'C\x00\x00\x00\x00\x00\x00\x00' or b'C\x00\x00\x00\x00\x00\xe0?'.
		# AFAIK the curvature types are always one character long anyway.
		ctype = curv[7].tobytes()[:1].decode('utf-8')
		# fill in the data structure
		data.elem[iel].curv[iedge, :] = cparams
		data.elem[iel].ccurv[iedge] = ctype
	#
	# read boundary conditions
	# there can be more than one field, and we won't know until we'vre reached the end
	nbcparam = 8
	buf = infile.read(wdsz)
	ifield = 0
	while buf != b'':
		# the data is initialized with one BC field, we might need to allocate another
		if ifield > 0:
			data.nbc = data.nbc+1
			for el in data.elem:
				empty_bcs = np.zeros(el.bcs[:1, :].shape, dtype=el.bcs.dtype)
				el.bcs = np.concatenate((el.bcs, empty_bcs))
		nbclines = int(np.frombuffer(buf)[0])
		logger.debug('Found {} external boundary conditions for field {}'.format(nbclines, ifield))
		buf = infile.read(wdsz*(nbcparam*nbclines))
		for ibc in range(nbclines):
			# interpret the data
			bc = np.frombuffer(buf, dtype=emode+realtype, count=nbcparam, offset=ibc*nbcparam*wdsz)
			iel = int(bc[0])-1
			iface = int(bc[1])-1
			bcparams = bc[2:7]
			bctype = bc[7].tobytes().decode('utf-8').rstrip()  # remove trailing spaces
			# fill in the data structure
			data.elem[iel].bcs[ifield, iface][0] = bctype
			data.elem[iel].bcs[ifield, iface][1] = iel+1
			data.elem[iel].bcs[ifield, iface][2] = iface+1
			for ipar in range(5):
				data.elem[iel].bcs[ifield, iface][3+ipar] = bcparams[ipar]
		ifield = ifield + 1
		# try reading the number of conditions in the next field
		buf = infile.read(wdsz)
	infile.close()
	return data


def writere2(fname, data):
	"""A function for writing binary .re2 files for nek5000

	Parameters
	----------
	fname : str
		file name
	data : exadata
		data organised as in exadata.py
	"""
	#
	#---------------------------------------------------------------------------
	# CHECK INPUT DATA
	#---------------------------------------------------------------------------
	#
	# We could extract the corners, but for now just return an error if lr1 is too large
	if data.lr1 != [2, 2, data.ndim-1]:
		logger.critical('wrong element dimensions for re2 file! {} != {}'.format(data.lr1, [2, 2, data.ndim-1]))
		return -2
	#
	if data.var[0] != data.ndim:
		logger.critical('wrong number of geometric variables for re2 file! expected {}, found {}'.format(data.ndim, data.var[0]))
		return -3
	#
	# Open file
	try:
		outfile = open(fname, 'wb')
	except IOError as e:
		logger.critical('I/O error ({0}): {1}'.format(e.errno, e.strerror))
		return -1
	#
	#---------------------------------------------------------------------------
	# WRITE HEADER
	#---------------------------------------------------------------------------
	#
	# always double precision
	wdsz = 8
	realtype = 'd'
	nel = data.nel
	ndim = data.ndim
	header = '#v002{:9d}{:3d}{:9d} this is the hdr'.format(nel, ndim, nel)
	header = header.ljust(80)
	outfile.write(header.encode('utf-8'))
	#
	# decide endianness
	if data.endian in ('big', 'little'):
		byteswap = data.endian != sys.byteorder
		logger.debug(f'Writing {data.endian}-endian file')
	else:
		byteswap = False
		logger.warning(
			f'Unrecognized endianness {data.endian}, '
			f'writing native {sys.byteorder}-endian file'
		)
	#
	def correct_endianness(a):
		''' Return the array with the requested endianness'''
		if byteswap:
			return a.byteswap()
		else:
			return a
	#
	# write tag (to specify endianness)
	endianbytes = np.array([6.54321], dtype=np.float32)
	correct_endianness(endianbytes).tofile(outfile)
	#
	#---------------------------------------------------------------------------
	# WRITE DATA
	#---------------------------------------------------------------------------
	#
	# compute total number of points per element
	npel = 2**ndim
	#
	def write_data_to_file(a):
		'''Write the geometry of an element to the output file in double precision'''
		correct_endianness(a).tofile(outfile)
	#
	# write geometry (adding eight bytes of zeros before each element)
	xyz = np.zeros((npel*ndim+1,))  # array storing reordered geometry data (with a zero in the first position)
	for el in data.elem:
		# the data is stored in the following order (2D|3D):
		# x1, x2, x4, x3; | x5, x6, x8, x7;
		# y1, y2, y4, y3; | y5, y6, y8, y7;
		# ----------------
		# z1, z2, z4, z3; z5, z6, z8, z7;
		# where 1-8 is the ordering of the points in memory
		for idim in range(ndim):  # x, y, [z]
			for iz in range(ndim-1):  # this does only one iteration in 2D, and in 3D does one iteration for 1-4 and one for 5-8
				xyz[npel*idim+4*iz+1] = el.pos[idim, iz, 0, 0]
				xyz[npel*idim+4*iz+2] = el.pos[idim, iz, 0, 1]
				xyz[npel*idim+4*iz+3] = el.pos[idim, iz, 1, 1]
				xyz[npel*idim+4*iz+4] = el.pos[idim, iz, 1, 0]
		write_data_to_file(xyz)
	#
	# write curve sides data
	# locate curved edges
	curved_edges = []
	for (iel, el) in enumerate(data.elem):
		for iedge in range(12):
			if el.ccurv[iedge] != '':
				curved_edges.append((iel, iedge))
	# write number of curved edges
	ncurv = len(curved_edges)
	if ncurv != data.ncurv:
		logger.warning('wrong number of curved edges: expected {}, found {}'.format(data.ncurv, ncurv))
	ncurvf = np.array([ncurv], dtype=np.float64)
	write_data_to_file(ncurvf)
	# format curve data
	cdata = np.zeros((ncurv,), dtype='f8, f8, f8, f8, f8, f8, f8, S8')
	for (cdat, (iel, iedge)) in zip(cdata, curved_edges):
		el = data.elem[iel]
		cdat[0] = iel+1
		cdat[1] = iedge+1
		# curve parameters
		for j in range(5):
			cdat[2+j] = el.curv[iedge, j]
		# encode the string as a byte array padded with spaces
		cdat[7] = el.ccurv[iedge].encode('utf-8')
	# write to file
	write_data_to_file(cdata)
	#
	# write boundary conditions for each field
	for ifield in range(data.nbc):
		# locate faces with boundary conditions
		bc_faces = []
		for (iel, el) in enumerate(data.elem):
			for iface in range(2*ndim):
				bctype = el.bcs[ifield, iface][0]
				# internal boundary conditions are not written to .re2 files by reatore2
				# and are apparently ignored by Nek5000 even in .rea files
				if bctype != '' and bctype != 'E':
					bc_faces.append((iel, iface))
		nbcs = len(bc_faces)
		nbcsf = np.array([nbcs], dtype=np.float64)
		write_data_to_file(nbcsf)
		# initialize and format data
		bcdata = np.zeros((nbcs,), dtype='f8, f8, f8, f8, f8, f8, f8, S8')
		for (bc, (iel, iface)) in zip(bcdata, bc_faces):
			el = data.elem[iel]
			bc[0] = iel+1
			bc[1] = iface+1
			for j in range(5):
				bc[2+j] = el.bcs[ifield, iface][3+j]
			# encode the string as a byte array padded with spaces
			bc[7] = el.bcs[ifield, iface][0].encode('utf-8').ljust(8)
		# write to file
		write_data_to_file(bcdata)
	#
	# close file
	outfile.close()
	# rerurn
	return 0
