from __future__ import annotations

import struct
import sys

import numpy as np
from pymech.core import HexaData
from pymech.log import logger


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

    Parameters
    ----------
    fname : str
            file name
    """
    #
    try:
        infile = open(fname)
    except OSError as e:
        logger.critical(f"I/O error ({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 "NO" not 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 = HexaData(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.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 : :class:`pymech.core.HexaData`
            data structure
    """
    #
    try:
        outfile = open(fname, "w")
    except OSError as e:
        logger.critical(f"I/O error ({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(f"   {data.ndim:1d} DIMENSIONAL RUN\n")
    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(f"  {data.nel:10d} {data.ndim:1d} {data.nel:10d} NEL,NDIM,NELV\n")
    for iel in range(data.nel):
        outfile.write("           ELEMENT {:10d} [  1a]    GROUP 1\n".format(iel + 1))
        if data.ndim == 2:
            outfile.write(
                "{:14.6e}{:14.6e}{:14.6e}{: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(
                "{:14.6e}{:14.6e}{:14.6e}{: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(
                "{:14.6e}{:14.6e}{:14.6e}{: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(
                "{:14.6e}{:14.6e}{:14.6e}{: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(
                "{:14.6e}{:14.6e}{:14.6e}{: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(
                "{:14.6e}{:14.6e}{:14.6e}{: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(
                "{:14.6e}{:14.6e}{:14.6e}{: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(
                "{:14.6e}{:14.6e}{:14.6e}{: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(
        f"  {data.ncurv:10d} Curved sides follow IEDGE,IEL,CURVE(I),I=1,5, CCURVE\n"
    )
    for iel in range(data.nel):
        if data.nel < 1e3:
            #
            for iedge in range(12):
                if data.elem[iel].ccurv[iedge] != "":
                    outfile.write(
                        "{:3d}{:3d}{:14.6e}{:14.6e}{:14.6e}{:14.6e}{:14.6e}{:>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(
                        "{:2d}{:6d}{:14.6e}{:14.6e}{:14.6e}{:14.6e}{:14.6e}{:>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(
                        "{:2d}{:10d}{:14.6e}{:14.6e}{:14.6e}{:14.6e}{:14.6e}{:>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(
                        " {:2s} {:3d}{:3d}{:14.6e}{:14.6e}{:14.6e}{:14.6e}{: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(
                        " {:2s} {:6d}{:14.6e}{:14.6e}{:14.6e}{:14.6e}{: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(
                        " {:2s} {:11d}{:1d}{:18.11e}{:18.11e}{:18.11e}{:18.11e}{: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 OSError as e:
        logger.critical(f"I/O error ({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 = HexaData(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(f"Found {ncurv} curved sides")
    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(
            f"Found {nbclines} external boundary conditions for field {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 : :class:`pymech.core.HexaData`
            data structure
    """
    #
    # ---------------------------------------------------------------------------
    # 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 OSError as e:
        logger.critical(f"I/O error ({e.errno}): {e.strerror}")
        return -1
    #
    # ---------------------------------------------------------------------------
    # WRITE HEADER
    # ---------------------------------------------------------------------------
    #
    # always double precision
    #  wdsz = 8
    #  realtype = 'd'
    nel = data.nel
    ndim = data.ndim
    header = f"#v002{nel:9d}{ndim:3d}{nel:9d} this is the hdr"
    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(
            f"wrong number of curved edges: expected {data.ncurv}, found {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
