using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using CaeMesh;
using CaeGlobals;
using vtkControl;

namespace CaeResults
{
    public enum OpenFoamFieldType
    {
        Unknown,
        SurfaceScalarField,
        VolScalarField,
        VolVectorField
    }
    public enum OpenFoamFormatType
    {
        Unknown,
        Ascii,
        Binary
    }
    public class OpenFoamHeader
    {
        public string Version;
        public OpenFoamFormatType FormatType;
        public OpenFoamFieldType FieldType;

        public OpenFoamHeader()
        {
            Version = null;
            FormatType = OpenFoamFormatType.Unknown;
            FieldType = OpenFoamFieldType.Unknown;
        }
    }
    //                                                                      
    // https://doc.cfd.direct/openfoam/user-guide-v6/basic-file-format      
    //                                                                      
    public static class OpenFoamFileReader
    {
        // Methods                                                                                                                  
        static public FeResults Read(string fileName)
        {
            if (fileName != null && File.Exists(fileName))
            {
                string baseFolder = Path.GetDirectoryName(fileName);
                string meshFolder = Path.Combine(baseFolder, "constant", "polyMesh");
                if (Directory.Exists(meshFolder))
                {
                    string pointsFile = Path.Combine(meshFolder, "points");
                    string boundaryFile = Path.Combine(meshFolder, "boundary");
                    string facesFile = Path.Combine(meshFolder, "faces");
                    string ownerFile = Path.Combine(meshFolder, "owner");
                    //
                    if (File.Exists(pointsFile) && File.Exists(boundaryFile) && File.Exists(facesFile)  && File.Exists(ownerFile))
                    {
                        // Read mesh
                        Dictionary<int, HashSet<int>> faceIdNodeIds;
                        HashSet<int> allFaceNodeIds;
                        HashSet<int> boundaryFaceIds = GetBoundaryFaceIds(boundaryFile);
                        Dictionary<int, FeElement> elements = GetBoundariesAsTriangles(facesFile, boundaryFaceIds,
                                                                                       out faceIdNodeIds, out allFaceNodeIds);
                        Dictionary<int, HashSet<int>> cellIdNodeIds = GetCellIdNodeIds(ownerFile, boundaryFaceIds, faceIdNodeIds);
                        Dictionary<int, int> nodeIdCellCount = GetNodeIdCellCount(cellIdNodeIds);
                        //
                        int numOfAllNodes;
                        Dictionary<int, int> nodeIdsLookUp;
                        Dictionary<int, FeNode> nodes = GetNodes(pointsFile, allFaceNodeIds, out numOfAllNodes, out nodeIdsLookUp);
                        //
                        FeMesh mesh = new FeMesh(nodes, elements, MeshRepresentation.Results);
                        mesh.ResetPartsColor();
                        // Read results
                        FeResults result = new FeResults(fileName);
                        result.SetMesh(mesh, nodeIdsLookUp);
                        //
                        int globalIncrementId = 1;
                        int stepId = 1;
                        FieldData fieldData;
                        Field field;
                        string[] resultFileNames;
                        Dictionary<double, string> timeResultFolderNames = GerTimeResultFolderNames(baseFolder);
                        double[] sortedTimes = timeResultFolderNames.Keys.ToArray();
                        Array.Sort(sortedTimes);
                        //
                        foreach (double sortedTime in sortedTimes)
                        {
                            resultFileNames = Directory.GetFiles(timeResultFolderNames[sortedTime]);
                            foreach (var resultFileName in resultFileNames)
                            {
                                try
                                {
                                    GetField(resultFileName, sortedTime, globalIncrementId, stepId, numOfAllNodes,
                                             cellIdNodeIds, nodeIdsLookUp, nodeIdCellCount, out fieldData, out field);                                    
                                    //
                                    if (field != null) result.AddField(fieldData, field);
                                }
                                catch
                                { }
                                //
                                globalIncrementId++;
                            }
                            //
                            stepId++;
                        }
                        //
                        return result;
                    }
                }
            }
            //
            return null;
        }
        static public FeResults Read(string fileName, double time, string variableName)
        {
            if (fileName != null && File.Exists(fileName))
            {
                string baseFolder = Path.GetDirectoryName(fileName);
                string meshFolder = Path.Combine(baseFolder, "constant", "polyMesh");
                if (Directory.Exists(meshFolder))
                {
                    string pointsFile = Path.Combine(meshFolder, "points");         // coordinates of the mehs nodes
                    string boundaryFile = Path.Combine(meshFolder, "boundary");     // names and face ids of boundary faces
                    string facesFile = Path.Combine(meshFolder, "faces");           // topology of the faces
                    string ownerFile = Path.Combine(meshFolder, "owner");           // face ids 
                    //
                    if (File.Exists(pointsFile) && File.Exists(boundaryFile) && File.Exists(facesFile) && File.Exists(ownerFile))
                    {
                        // Read mesh
                        Dictionary<int, HashSet<int>> faceIdNodeIds;
                        HashSet<int> allFaceNodeIds;
                        HashSet<int> boundaryFaceIds = GetBoundaryFaceIds(boundaryFile);
                        Dictionary<int, FeElement> elements = GetBoundariesAsTriangles(facesFile, boundaryFaceIds,
                                                                                       out faceIdNodeIds, out allFaceNodeIds);
                        Dictionary<int, HashSet<int>> cellIdNodeIds = GetCellIdNodeIds(ownerFile, boundaryFaceIds, faceIdNodeIds);
                        Dictionary<int, int> nodeIdCellCount = GetNodeIdCellCount(cellIdNodeIds);
                        //
                        int numOfAllNodes;
                        Dictionary<int, int> nodeIdsLookUp;
                        Dictionary<int, FeNode> nodes = GetNodes(pointsFile, allFaceNodeIds, out numOfAllNodes, out nodeIdsLookUp);
                        //
                        FeMesh mesh = new FeMesh(nodes, elements, MeshRepresentation.Results);
                        mesh.ResetPartsColor();
                        // Read results
                        FeResults result = new FeResults(fileName);
                        result.SetMesh(mesh, nodeIdsLookUp);
                        //
                        int globalIncrementId = 1;
                        int stepId = 1;
                        FieldData fieldData;
                        Field field;
                        string[] resultFileNames;
                        Dictionary<double, string> timeResultFolderNames = GerTimeResultFolderNames(baseFolder);
                        if (!timeResultFolderNames.ContainsKey(time))
                            throw new CaeException("The selected OpenFOAM folder does not contain results for time: " + time + ".");
                        //
                        resultFileNames = Directory.GetFiles(timeResultFolderNames[time]);
                        foreach (var resultFileName in resultFileNames)
                        {
                            try
                            {
                                if (Path.GetFileNameWithoutExtension(resultFileName) == variableName)
                                {
                                    GetField(resultFileName, time, globalIncrementId, stepId, numOfAllNodes, cellIdNodeIds,
                                             nodeIdsLookUp, nodeIdCellCount, out fieldData, out field);
                                    //
                                    if (field != null) result.AddField(fieldData, field);
                                }
                            }
                            catch
                            { }
                            //
                            globalIncrementId++;
                        }
                        //
                        stepId++;
                        //
                        return result;
                    }
                }
            }
            //
            return null;
        }
        static public Dictionary<string, string[]> GetTimeResultScalarVariableNames(string fileName)
        {
            Dictionary<string, string[]> timeResultVariableNames = new Dictionary<string, string[]>();
            //
            if (fileName != null && File.Exists(fileName))
            {
                OpenFoamHeader header;
                string[] resultFileNames;
                HashSet<string> variableNamesHash = new HashSet<string>();
                //
                string baseFolder = Path.GetDirectoryName(fileName);
                Dictionary<double, string> timeResultFolderNames = GerTimeResultFolderNames(baseFolder);
                double[] sortedTimes = timeResultFolderNames.Keys.ToArray();
                Array.Sort(sortedTimes);
                //
                foreach (double sortedTime in sortedTimes)
                {
                    variableNamesHash.Clear();
                    resultFileNames = Directory.GetFiles(timeResultFolderNames[sortedTime]);
                    foreach (var resultFileName in resultFileNames)
                    {
                        header = GetHeaderFromFile(resultFileName);
                        if (header.FieldType == OpenFoamFieldType.VolScalarField)
                            variableNamesHash.Add(Path.GetFileNameWithoutExtension(resultFileName));
                    }
                    timeResultVariableNames.Add(sortedTime.ToString(), variableNamesHash.ToArray());
                }
            }
            //
            return timeResultVariableNames;
        }
        // Mesh
        static private OpenFoamHeader GetHeaderFromFile(string fileName)
        {
            bool foamFile = false;
            bool endBracket = false;
            string line;
            List<string> lines = new List<string>();
            OpenFoamHeader header = null;
            //
            if (!Tools.WaitForFileToUnlock(fileName, 5000)) return null;
            //
            using (FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            using (StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8, true, 16 * 4096))
            {
                while (!streamReader.EndOfStream)
                {
                    line = streamReader.ReadLine().Trim().ToUpper();
                    lines.Add(line);
                    //
                    if (line.StartsWith("FOAMFILE")) foamFile = true;
                    if (line.StartsWith("}")) endBracket = true;
                    //
                    if (lines.Count > 15 && !foamFile) break;
                    if (endBracket)
                    {
                        int lineId = 0;
                        //
                        string[] linesArr = SkipCommentsAndEmptyLines(lines.ToArray());
                        header = GetFoamHeader(linesArr, ref lineId);
                        break;
                    }
                }
                //
                streamReader.Close();
                fileStream.Close();
            }
            //
            return header;
        }
        static private Dictionary<int, FeNode> GetNodes(string fileName, HashSet<int> allFaceNodeIds,
                                                        out int numOfAllNodes, out Dictionary<int, int> nodeIdsLookUp)
        {
            OpenFoamHeader header = GetHeaderFromFile(fileName);
            //
            if (header.FormatType == OpenFoamFormatType.Ascii)
                return GetNodesAscii(fileName, allFaceNodeIds, out numOfAllNodes, out nodeIdsLookUp);
            else if (header.FormatType == OpenFoamFormatType.Binary)
                return GetNodesBinary(fileName, allFaceNodeIds, out numOfAllNodes, out nodeIdsLookUp);
            else throw new NotSupportedException();
        }
        static private Dictionary<int, FeNode> GetNodesAscii(string fileName, HashSet<int> allFaceNodeIds,
                                                             out int numOfAllNodes, out Dictionary<int, int> nodeIdsLookUp)
        {
            string[] lines = Tools.ReadAllLines(fileName);
            //
            int lineId = 0;
            lines = SkipCommentsAndEmptyLines(lines);
            GetFoamHeader(lines, ref lineId);
            //
            string[] tmp;
            string[] splitter = new string[] { "(", ",", " ", ")" };
            FeNode node;
            Dictionary<int, FeNode> nodes = new Dictionary<int, FeNode>();
            nodeIdsLookUp = new Dictionary<int, int>();
            //
            numOfAllNodes = int.Parse(lines[lineId++]);
            // Skip bracket
            if (lines[lineId].StartsWith("(")) lineId++;
            //
            for (int i = 0; i < numOfAllNodes; i++)
            {
                if (allFaceNodeIds.Contains(i))
                {
                    tmp = lines[lineId].Split(splitter, StringSplitOptions.RemoveEmptyEntries);
                    if (tmp.Length == 3)
                    {
                        node = new FeNode(i, double.Parse(tmp[0]), double.Parse(tmp[1]), double.Parse(tmp[2]));
                        nodes.Add(i, node);
                        nodeIdsLookUp.Add(i, nodeIdsLookUp.Count);
                    }
                    else throw new NotSupportedException();
                }
                //
                lineId++;
            }
            //
            return nodes;
        }
        static private Dictionary<int, FeNode> GetNodesBinary(string fileName, HashSet<int> allFaceNodeIds,
                                                             out int numOfAllNodes, out Dictionary<int, int> nodeIdsLookUp)
        {
            numOfAllNodes = -1;
            double[] nodeData = null;
            Dictionary<int, FeNode> nodes = new Dictionary<int, FeNode>();
            nodeIdsLookUp = new Dictionary<int, int>();
            //
            if (!Tools.WaitForFileToUnlock(fileName, 5000)) return nodes;
            //
            using (FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            using (BinaryReader binaryReader = new BinaryReader(fileStream))
            {
                // Skip header
                if (SkipHeader(binaryReader))
                {
                    // Nodes
                    numOfAllNodes = GetNextIntFromLine(binaryReader);
                    nodeData = ReadDoubleArray(binaryReader, 3 * numOfAllNodes);
                }
                binaryReader.Close();
                fileStream.Close();
            }
            //
            if (nodeData != null)
            {
                FeNode node;
                for (int i = 0; i < numOfAllNodes; i++)
                {
                    if (allFaceNodeIds.Contains(i))
                    {
                        node = new FeNode(i, nodeData[3 * i], nodeData[3 * i + 1], nodeData[3 * i + 2]);
                        nodes.Add(i, node);
                        nodeIdsLookUp.Add(i, nodeIdsLookUp.Count);
                    }
                }
            }
            //
            return nodes;
        }
        static private HashSet<int> GetBoundaryFaceIds(string fileName)
        {
            // Ascii and binary boundary file is the same
            //OpenFoamHeader boundaryHeader = GetHeaderFromFile(fileName);
            //
            string[] lines = Tools.ReadAllLines(fileName);
            //
            int lineId = 0;
            lines = SkipCommentsAndEmptyLines(lines);
            GetFoamHeader(lines, ref lineId);
            //
            int numOfBoundaries;
            string line;
            // Read the number of boundaries
            numOfBoundaries = int.Parse(lines[lineId++].Split('(')[0]);
            // Get boundaries
            List<string> boundary = new List<string>();
            List<List<string>> boundaries = new List<List<string>>();
            //
            for (int i = lineId; i < lines.Length; i++)
            {
                line = lines[i];
                //
                if (line[0] == '(' || line[0] == ')' || line[0] == '{' || line[0] == '}')
                {
                    if (boundary.Count > 0)
                    {
                        boundaries.Add(boundary);
                        boundary = new List<string>();
                    }
                }
                else
                {
                    boundary.Add(line);
                }
            }
            // Get ids
            string[] tmp;
            string[] splitter = new string[] { " ", ";" };
            int numberOfFaces;
            int startFaceId;
            HashSet<int> boundaryFaceIds = new HashSet<int>();
            foreach (var boundaryLines in boundaries)
            {
                numberOfFaces = -1;
                startFaceId = -1;
                // Boundary name
                if (boundaryLines.Count <= 1)
                { }
                else
                {
                    foreach (var boundaryLine in boundaryLines)
                    {
                        tmp = boundaryLine.ToUpper().Split(splitter, StringSplitOptions.RemoveEmptyEntries);
                        if (tmp[0] == "NFACES") numberOfFaces = int.Parse(tmp[1]);
                        else if (tmp[0] == "STARTFACE") startFaceId = int.Parse(tmp[1]);
                    }
                }
                //
                if (numberOfFaces > 0 && startFaceId > 0)
                {
                    for (int i = startFaceId; i < startFaceId + numberOfFaces; i++) boundaryFaceIds.Add(i);
                }
            }
            //
            return boundaryFaceIds;
        }
        //
        static private Dictionary<int, FeElement> GetBoundariesAsTriangles(string fileName, HashSet<int> boundaryFaceIds,
                                                                           out Dictionary<int, HashSet<int>> faceIdNodeIds,
                                                                           out HashSet<int> allFaceNodeIds)
        {
            OpenFoamHeader header = GetHeaderFromFile(fileName);
            //
            if (header.FormatType == OpenFoamFormatType.Ascii)
                return GetBoundariesAsTrianglesAscii(fileName, boundaryFaceIds, out faceIdNodeIds, out allFaceNodeIds);
            else if (header.FormatType == OpenFoamFormatType.Binary)
                return GetBoundariesAsTrianglesBinary(fileName, boundaryFaceIds, out faceIdNodeIds, out allFaceNodeIds);
            else throw new NotSupportedException();
        }
        static private Dictionary<int, FeElement> GetBoundariesAsTrianglesAscii(string fileName, HashSet<int> boundaryFaceIds,
                                                                                out Dictionary<int, HashSet<int>> faceIdNodeIds,
                                                                                out HashSet<int> allFaceNodeIds)
        {
            // Read only faces on the boundary
            string[] lines = Tools.ReadAllLines(fileName);
            //
            int lineId = 0;
            lines = SkipCommentsAndEmptyLines(lines);
            GetFoamHeader(lines, ref lineId);
            //
            string[] tmp;
            string[] splitter = new string[] { "(", ",", " ", ")" };
            int numOfNodes;
            int[] nodeIds;
            int[] faceNodeIds;
            int numOfFaces;
            int triangleId = 0;
            FeElement triangle;
            Dictionary<int, FeElement> triangles = new Dictionary<int, FeElement>();
            // Read number of faces
            numOfFaces = int.Parse(lines[lineId++]);
            // Skip bracket
            if (lines[lineId].StartsWith("(")) lineId++;
            //
            HashSet<int> faceNodeIdsHash;
            faceIdNodeIds = new Dictionary<int, HashSet<int>>();
            allFaceNodeIds = new HashSet<int>();
            //
            for (int i = 0; i < numOfFaces; i++)
            {
                if (boundaryFaceIds.Contains(i))
                {
                    tmp = lines[lineId].Split(splitter, StringSplitOptions.RemoveEmptyEntries);
                    //
                    if (int.TryParse(tmp[0], out numOfNodes))
                    {
                        if (tmp.Length == numOfNodes + 1)
                        {
                            faceNodeIds = new int[tmp.Length - 1];
                            for (int j = 0; j < tmp.Length - 1; j++) faceNodeIds[j] = int.Parse(tmp[j + 1]);
                            // Read boudary as triangles
                            for (int j = 0; j < faceNodeIds.Length - 2; j++)
                            {
                                nodeIds = new int[] { faceNodeIds[0], faceNodeIds[j + 1], faceNodeIds[j + 2] };
                                triangle = new LinearTriangleElement(triangleId++, nodeIds);
                                triangles.Add(triangle.Id, triangle);
                            }
                            //
                            faceNodeIdsHash = new HashSet<int>(faceNodeIds);
                            faceIdNodeIds.Add(i, faceNodeIdsHash);
                            allFaceNodeIds.UnionWith(faceNodeIdsHash);
                        }
                        else throw new NotSupportedException();
                    }
                }
                //
                lineId++;
            }
            //
            return triangles;
        }
        static private Dictionary<int, FeElement> GetBoundariesAsTrianglesBinary(string fileName, HashSet<int> boundaryFaceIds,
                                                                                 out Dictionary<int, HashSet<int>> faceIdNodeIds,
                                                                                 out HashSet<int> allFaceNodeIds)
        {
            int numOfFaces = -1;
            int[] indices = null;       // 3 faces      :   {0, 3, 6, 8} -> 0..3, 3..6, 6..8
            int[] allNodeIds = null;    // all node ids :   {0, 1, 2, 5, 6, 8, 12, 13, 14}
            faceIdNodeIds = new Dictionary<int, HashSet<int>>();
            allFaceNodeIds = new HashSet<int>();
            Dictionary<int, FeElement> triangles = new Dictionary<int, FeElement>();
            //
            if (!Tools.WaitForFileToUnlock(fileName, 5000)) return triangles;
            //
            using (FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            using (BinaryReader binaryReader = new BinaryReader(fileStream))
            {
                // Skip header
                if (SkipHeader(binaryReader))
                {
                    // Offsets
                    int numOfIndices = GetNextIntFromLine(binaryReader);
                    indices = ReadIntArray(binaryReader, numOfIndices);
                    numOfFaces = numOfIndices - 1;   // remove the last index which determines the end index of the last face
                    // Ids
                    int numOfNodeIds = GetNextIntFromLine(binaryReader);
                    allNodeIds = ReadIntArray(binaryReader, numOfNodeIds);
                }
                binaryReader.Close();
                fileStream.Close();
            }
            //
            if (indices != null && allNodeIds != null)
            {
                int[] nodeIds;
                int[] faceNodeIds;
                int triangleId = 0;
                HashSet<int> faceNodeIdsHash;
                LinearTriangleElement triangle;
                for (int i = 0; i < numOfFaces; i++)
                {
                    if (boundaryFaceIds.Contains(i))
                    {
                        faceNodeIds = new int[indices[i + 1] - indices[i]];
                        for (int j = 0; j < faceNodeIds.Length; j++) faceNodeIds[j] = allNodeIds[indices[i] + j];
                        // Read boudary as triangles
                        for (int j = 0; j < faceNodeIds.Length - 2; j++)
                        {
                            nodeIds = new int[] { faceNodeIds[0], faceNodeIds[j + 1], faceNodeIds[j + 2] };
                            triangle = new LinearTriangleElement(triangleId++, nodeIds);
                            triangles.Add(triangle.Id, triangle);
                        }
                        //
                        faceNodeIdsHash = new HashSet<int>(faceNodeIds);
                        faceIdNodeIds.Add(i, faceNodeIdsHash);
                        allFaceNodeIds.UnionWith(faceNodeIdsHash);
                    }
                }
            }
            //
            return triangles;
        }
        //
        static private Dictionary<int, HashSet<int>> GetCellIdNodeIds(string fileName, HashSet<int> boundaryFaceIds,
                                                                      Dictionary<int, HashSet<int>> faceIdNodeIds)
        {
            OpenFoamHeader header = GetHeaderFromFile(fileName);
            //
            if (header.FormatType == OpenFoamFormatType.Ascii)
                return GetCellIdNodeIdsAscii(fileName, boundaryFaceIds, faceIdNodeIds);
            else if (header.FormatType == OpenFoamFormatType.Binary)
                return GetCellIdNodeIdsBinary(fileName, boundaryFaceIds, faceIdNodeIds);
            else throw new NotSupportedException();
        }
        static private Dictionary<int, HashSet<int>> GetCellIdNodeIdsAscii(string fileName, HashSet<int> boundaryFaceIds,
                                                                           Dictionary<int, HashSet<int>> faceIdNodeIds)
        {
            // Use only faces on the boundary
            string[] lines = Tools.ReadAllLines(fileName);
            //
            int lineId = 0;
            lines = SkipCommentsAndEmptyLines(lines);
            GetFoamHeader(lines, ref lineId);
            //
            int cellId;
            int numOfFaces;
            HashSet<int> cellNodeIds;
            Dictionary<int, HashSet<int>> cellIdNodeIds = new Dictionary<int, HashSet<int>>();
            // Read number of faces
            numOfFaces = int.Parse(lines[lineId++]);
            // Skip bracket
            if (lines[lineId].StartsWith("(")) lineId++;
            //
            for (int i = 0; i < numOfFaces; i++)
            {
                if (boundaryFaceIds.Contains(i))
                {
                    cellId = int.Parse(lines[lineId]);
                    if (cellIdNodeIds.TryGetValue(cellId, out cellNodeIds)) cellNodeIds.UnionWith(faceIdNodeIds[i]);
                    else cellIdNodeIds.Add(cellId, new HashSet<int>(faceIdNodeIds[i]));
                }
                //
                lineId++;
            }
            return cellIdNodeIds;
        }
        static private Dictionary<int, HashSet<int>> GetCellIdNodeIdsBinary(string fileName, HashSet<int> boundaryFaceIds,
                                                                           Dictionary<int, HashSet<int>> faceIdNodeIds)
        {
            int numOfFaces = -1;
            int[] faceIdCellIds = null;
            HashSet<int> cellNodeIds;
            Dictionary<int, HashSet<int>> cellIdNodeIds = new Dictionary<int, HashSet<int>>();
            //
            if (!Tools.WaitForFileToUnlock(fileName, 5000)) return cellIdNodeIds;
            //
            using (FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            using (BinaryReader binaryReader = new BinaryReader(fileStream))
            {
                // Skip header
                if (SkipHeader(binaryReader))
                {
                    // Face id - Cell id
                    numOfFaces = GetNextIntFromLine(binaryReader);
                    faceIdCellIds = ReadIntArray(binaryReader, numOfFaces);
                }
                binaryReader.Close();
                fileStream.Close();
            }
            //
            if (faceIdCellIds != null)
            {
                int cellId;
                for (int i = 0; i < numOfFaces; i++)
                {
                    if (boundaryFaceIds.Contains(i))
                    {
                        cellId = faceIdCellIds[i];
                        if (cellIdNodeIds.TryGetValue(cellId, out cellNodeIds)) cellNodeIds.UnionWith(faceIdNodeIds[i]);
                        else cellIdNodeIds.Add(cellId, new HashSet<int>(faceIdNodeIds[i]));
                    }
                }
            }
            //
            return cellIdNodeIds;
        }
        //
        static private Dictionary<int, int> GetNodeIdCellCount(Dictionary<int, HashSet<int>> cellIdNodeIds)
        { 
            Dictionary<int, int> nodeIdCellCount = new Dictionary<int, int>();
            foreach (var entry in cellIdNodeIds)
            {
                foreach (var nodeId in entry.Value)
                {
                    if (nodeIdCellCount.ContainsKey(nodeId)) nodeIdCellCount[nodeId]++;
                    else nodeIdCellCount.Add(nodeId, 1);
                }
            }
            return nodeIdCellCount;
        }
        
        // Results
        static private Dictionary<double, string> GerTimeResultFolderNames(string baseFolder)
        {
            double time;
            string directoryName;
            Dictionary<double, string> timeResultFolderNames = new Dictionary<double, string>();
            //
            foreach (var directory in Directory.GetDirectories(baseFolder))
            {
                directoryName = Path.GetFileNameWithoutExtension(directory);
                //
                if (double.TryParse(directoryName, out time) && time > 0)
                {
                    timeResultFolderNames.Add(time, directory);
                }
            }
            return timeResultFolderNames;
        }
        static private void GetField(string fileName, double time, int globalIncrementId, int stepId,
                                     int numOfAllNodes,
                                     Dictionary<int, HashSet<int>> cellIdNodeIds,
                                     Dictionary<int, int> nodeIdsLookUp,
                                     Dictionary<int, int> nodeIdCellCount,
                                     out FieldData fieldData, out Field field)
        {
            fieldData = new FieldData(Path.GetFileNameWithoutExtension(fileName));
            fieldData.GlobalIncrementId = globalIncrementId;
            fieldData.StepType = StepTypeEnum.Static;
            fieldData.Time = (float)time;
            fieldData.MethodId = 1;
            fieldData.StepId = stepId;
            fieldData.StepIncrementId = 1;
            //
            field = null;
            // Header
            OpenFoamHeader header = GetHeaderFromFile(fileName);
            // This field types are not supported
            if (header.FieldType == OpenFoamFieldType.Unknown || header.FieldType == OpenFoamFieldType.SurfaceScalarField)
                return;
            // Read values
            float[][] cellValues;
            if (header.FormatType == OpenFoamFormatType.Ascii)
                cellValues = GetCellValuesAscii(fileName, header, numOfAllNodes);
            else if (header.FormatType == OpenFoamFormatType.Binary)
                cellValues = GetCellValuesBinary(fileName, header, numOfAllNodes);
            else throw new NotSupportedException();
            //
            if (cellValues != null)
            {
                int numOfComponents = cellValues.Length;
                // Get sum of nodal values
                float[][] nodeValues = new float[numOfComponents][];
                for (int i = 0; i < numOfComponents; i++) nodeValues[i] = new float[nodeIdsLookUp.Count];
                //
                int localNodeId;
                foreach (var entry in cellIdNodeIds)
                {
                    foreach (var nodeId in entry.Value)
                    {
                        localNodeId = nodeIdsLookUp[nodeId];
                        for (int i = 0; i < numOfComponents; i++) nodeValues[i][localNodeId] += cellValues[i][entry.Key];
                    }
                }
                // Average nodal values
                foreach (var entry in nodeIdCellCount)
                {
                    localNodeId = nodeIdsLookUp[entry.Key];
                    for (int i = 0; i < numOfComponents; i++) nodeValues[i][localNodeId] /= entry.Value;
                }
                //
                field = new Field(fieldData.Name);
                string componentName;
                // Add vector magnitude
                if (header.FieldType == OpenFoamFieldType.VolVectorField && nodeValues.Length == 3 && nodeValues[0] != null)
                {
                    componentName = "ALL";
                    float[] magnitude = new float[nodeValues[0].Length];
                    for (int i = 0; i < nodeValues[0].Length; i++)
                    {
                        magnitude[i] = (float)Math.Sqrt(nodeValues[0][i] * nodeValues[0][i] +
                                                        nodeValues[1][i] * nodeValues[1][i]+
                                                        nodeValues[2][i] * nodeValues[2][i]);
                    }
                    field.AddComponent(componentName, magnitude);
                }
                // Add components
                for (int i = 0; i < numOfComponents; i++)
                {
                    if (numOfComponents == 1) componentName = "VAL";
                    else componentName = "VAL" + (i + 1).ToString();
                    field.AddComponent(componentName, nodeValues[i]);
                }
            }
        }
        static private float[][] GetCellValuesAscii(string fileName, OpenFoamHeader header, int numOfAllNodes)
        {
            // Read file
            string[] lines = Tools.ReadAllLines(fileName);
            //
            int lineId = 0;
            lines = SkipCommentsAndEmptyLines(lines);
            //
            string[] tmp;
            string[] splitter = new string[] { "(", ",", " ", ")", ";" };
            int numOfCells;
            int numOfComponents = -1;
            float[][] cellValues = null;
            string line;
            // Skip keywords until: internalField   nonuniform List<scalar>
            while (!lines[lineId].ToUpper().StartsWith("INTERNALFIELD")) lineId++;
            //
            line = lines[lineId].ToUpper();
            if (line.Contains("NONUNIFORM"))
            {
                lineId++;
                //
                numOfCells = int.Parse(lines[lineId++]);
                // Skip bracket
                if (lines[lineId].StartsWith("(")) lineId++;
                // Count the number of components
                tmp = lines[lineId].Split(splitter, StringSplitOptions.RemoveEmptyEntries);
                numOfComponents = tmp.Length;
                cellValues = new float[numOfComponents][];
                for (int i = 0; i < numOfComponents; i++) cellValues[i] = new float[numOfCells];
                // internalField   nonuniform List<scalar>
                if (header.FieldType == OpenFoamFieldType.VolScalarField)
                {
                    for (int i = 0; i < numOfCells; i++)
                    {
                        cellValues[0][i] = float.Parse(lines[lineId]);
                        lineId++;
                    }
                }
                // internalField   nonuniform List<vector> 
                else if (header.FieldType == OpenFoamFieldType.VolVectorField)
                {
                    for (int i = 0; i < numOfCells; i++)
                    {
                        tmp = lines[lineId].Split(splitter, StringSplitOptions.RemoveEmptyEntries);
                        for (int j = 0; j < numOfComponents; j++) cellValues[j][i] = float.Parse(tmp[j]);
                        lineId++;
                    }
                }
            }
            // internalField   uniform 1
            else if (line.Contains("UNIFORM"))
            {
                cellValues = ReadUniformField(line, numOfAllNodes);
            }
            //
            return cellValues;
        }
        static private float[][] GetCellValuesBinary(string fileName, OpenFoamHeader header, int numOfAllNodes)
        {
            int numOfValues = -1;
            int numOfComponents = -1;
            double[] values = null;
            float[][] cellValues = null;
            //
            if (!Tools.WaitForFileToUnlock(fileName, 5000)) return cellValues;
            //
            using (FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            using (BinaryReader binaryReader = new BinaryReader(fileStream))
            {
                // Skip header
                if (SkipHeader(binaryReader))
                {
                    // Skip keywords until: internalField   nonuniform List<scalar>
                    string line;
                    do line = ReadLineFromBinaryReader(binaryReader).ToUpper();
                    while (!line.StartsWith("INTERNALFIELD"));
                    //
                    if (line.Contains("NONUNIFORM"))
                    {
                        numOfValues = GetNextIntFromLine(binaryReader);
                        // internalField   nonuniform List<scalar>
                        if (header.FieldType == OpenFoamFieldType.VolScalarField)
                        {
                            numOfComponents = 1;
                            values = ReadDoubleArray(binaryReader, numOfValues);
                        }
                        // internalField   nonuniform List<vector> 
                        else if (header.FieldType == OpenFoamFieldType.VolVectorField)
                        {
                            numOfComponents = 3;
                            values = ReadDoubleArray(binaryReader, numOfComponents * numOfValues);
                        }
                    }
                    // internalField   uniform 1
                    else if (line.Contains("UNIFORM"))
                    {
                        return ReadUniformField(line, numOfAllNodes);
                    }
                }
                binaryReader.Close();
                fileStream.Close();
                //
                if (values != null)
                {
                    cellValues = new float[numOfComponents][];
                    for (int i = 0; i < numOfComponents; i++)
                    {
                        cellValues[i] = new float[numOfValues];
                        for (int j = 0; j < cellValues[i].Length; j++)
                        {
                            cellValues[i][j] = (float)values[numOfComponents * j + i];
                        }
                    }
                }
            }
            //
            return cellValues;
        }
        static private float[][] ReadUniformField(string line, int numOfAllNodes)
        {
            float[][] cellValues = null;
            string[] splitter = new string[] { "(", ",", " ", ")", ";" };
            string[] tmp = line.Split(splitter, StringSplitOptions.RemoveEmptyEntries);
            if (tmp.Length >= 2)
            {
                float value = float.Parse(tmp[2]);
                int numOfComponents = 1;
                cellValues = new float[numOfComponents][];
                for (int i = 0; i < numOfComponents; i++)
                {
                    cellValues[i] = new float[numOfAllNodes];
                    for (int j = 0; j < cellValues[i].Length; j++) cellValues[i][j] = value;
                }
            }
            return cellValues;
        }
        // File
        static private string[] SkipCommentsAndEmptyLines(string[] lines)
        {
            bool containsCommentDelimiters;
            bool singleLineComment = false;
            bool multiLineComment = false;
            string line;
            StringBuilder sb = new StringBuilder();
            List<string> dataLines = new List<string>(lines.Length);
            //
            for (int i = 0; i < lines.Length; i++)
            {
                line = lines[i].Trim();
                // Skip empty lines
                if (line.Length == 0) continue;
                // Comments
                singleLineComment = false;
                containsCommentDelimiters = false;
                //
                if (line.Length >= 2)
                {
                    for (int j = 0; j < line.Length - 1; j++)   // - 1 since the last char is inspected using j + 1
                    {
                        if (line[j] == '/')
                        {
                            if (line[j + 1] == '/')
                            {
                                containsCommentDelimiters = true;
                                break;
                            }
                            else if (line[j + 1] == '*')
                            {
                                containsCommentDelimiters = true;
                                break;
                            }
                        }
                        else if (line[j] == '*' && line[j + 1] == '/')
                        {
                            containsCommentDelimiters = true;
                            break;
                        }
                    }
                }
                // Add a line without comments
                if (!multiLineComment && !containsCommentDelimiters)
                {
                    dataLines.Add(line);
                }
                // Add a line with comments
                else
                {
                    sb.Clear();
                    for (int j = 0; j < line.Length; j++)
                    {
                        if (line[j] == '/' && j + 1 < line.Length)
                        { 
                            if (line[j + 1] == '/')
                            {
                                if (!multiLineComment) singleLineComment = true;
                            }
                            else if (line[j + 1] == '*')
                            {
                                if (!singleLineComment) multiLineComment = true;
                            }
                        }
                        else if (line[j] == '*' && j + 1 < line.Length && line[j + 1] == '/')
                        {
                            if (!singleLineComment && multiLineComment)
                            {
                                multiLineComment = false;
                                j++;
                                continue; // skip end comment characters
                            }
                        }
                        // Add non commented character
                        if (!singleLineComment && !multiLineComment) sb.Append(line[j]);
                    }
                    // Add line
                    if (sb.Length > 0) dataLines.Add(sb.ToString().Trim());
                }
            }
            //
            return dataLines.ToArray();
        }
        static private OpenFoamHeader GetFoamHeader(string[] lines, ref int lineId)
        {
            //FoamFile
            //{
            //    version     2.0;
            //    format binary;
            //    arch        "LSB;label=32;scalar=64";
            //    class vectorField;
            //    location    "constant/polyMesh";
            //    object points;
            //}
            string line;
            string[] tmp;
            string[] splitter = new string[] { " ", ";" };
            OpenFoamHeader header = new OpenFoamHeader();
            //
            line = lines[lineId].ToUpper();
            //
            if (line.StartsWith("FOAMFILE"))
            {
                while (!line.StartsWith("}"))
                {
                    //All lines are trimmed: class volScalarField;
                    if (line.StartsWith("VERSION"))
                    {
                        tmp = line.Split(splitter, StringSplitOptions.RemoveEmptyEntries);
                        if (tmp.Length == 2) header.Version = tmp[1];
                    }
                    else if (line.StartsWith("FORMAT"))
                    {
                        tmp = line.Split(splitter, StringSplitOptions.RemoveEmptyEntries);
                        if (tmp.Length == 2)
                        {
                            switch (tmp[1])
                            {
                                case "ASCII":
                                    header.FormatType = OpenFoamFormatType.Ascii;
                                    break;
                                case "BINARY":
                                    header.FormatType = OpenFoamFormatType.Binary;
                                    break;
                                default:
                                    break;
                            }
                        }
                    }
                    else if (line.StartsWith("CLASS"))
                    {
                        tmp = line.Split(splitter, StringSplitOptions.RemoveEmptyEntries);
                        if (tmp.Length == 2)
                        {
                            switch (tmp[1])
                            {
                                case "SURFACESCALARFIELD":
                                    header.FieldType = OpenFoamFieldType.SurfaceScalarField;
                                    break;
                                case "VOLSCALARFIELD":
                                    header.FieldType = OpenFoamFieldType.VolScalarField;
                                    break;
                                case "VOLVECTORFIELD":
                                    header.FieldType = OpenFoamFieldType.VolVectorField;
                                    break;
                                default:
                                    break;
                            }
                        }
                    }
                    lineId++;
                    line = lines[lineId].ToUpper();
                }
            }
            lineId++;
            //
            return header;
        }
        // Binary file
        static private bool SkipHeader(BinaryReader reader)
        {
            bool foamFile = false;
            bool endBracket = false;
            int lineCount = 0;
            string line;
            //
            while(reader.BaseStream.Position < reader.BaseStream.Length)
            {
                line = ReadLineFromBinaryReader(reader).ToUpper().TrimStart();
                if (line == null) return false;
                lineCount++;
                //
                if (line.StartsWith("FOAMFILE")) foamFile = true;
                if (line.StartsWith("}")) endBracket = true;
                //
                if (lineCount > 15 && !foamFile) break;
                if (endBracket)
                {
                    line = ReadLineFromBinaryReader(reader);
                    if (line == null) return false;
                    else return true;
                }
            }
            return false;
        }
        static public string ReadLineFromBinaryReader(BinaryReader reader)
        {
            if (reader.PeekChar() == -1) return null;
            //
            char c = ' ';
            StringBuilder sb = new StringBuilder();
            while (ReadChar(reader, ref c))
            {
                if (c == '\r' || c == '\n') return sb.ToString();
                else sb.Append(c);
            }
            //
            if (sb.Length > 0) return sb.ToString();
            else return null;
        }
        static private bool ReadChar(BinaryReader reader, ref char c)
        {
            if (reader.PeekChar() == -1) return false;
            c = reader.ReadChar();
            return true;
        }
        static private int GetNextIntFromLine(BinaryReader binaryReader)
        {
            int value = -1;
            string line;
            while (true)
            {
                line = ReadLineFromBinaryReader(binaryReader);
                if (line == null) return value; // end of stream
                if (int.TryParse(line, out value)) return value;
            }
        }
        static private int[] ReadIntArray(BinaryReader binaryReader, int numOfItems)
        {
            if (numOfItems > int.MaxValue / sizeof(int)) return ReadIntArrayByParts(binaryReader, numOfItems);
            else return ReadIntArrayAsOnePart(binaryReader, numOfItems);
        }
        static private int[] ReadIntArrayByParts(BinaryReader binaryReader, int numOfItems)
        {
            // Read parenthesis
            binaryReader.ReadChar();
            // Read data
            int maxLen = 1024 * 16;
            int numLoops = numOfItems / maxLen;
            int remainder = numOfItems % maxLen;
            //
            byte[] bytes;
            int[] allItems = new int[numOfItems];
            int[] items = new int[maxLen];
            for (int i = 0; i < numLoops; i++)
            {
                //bytes = new byte[maxLen * sizeof(int)];
                bytes = binaryReader.ReadBytes(maxLen * sizeof(int));
                Buffer.BlockCopy(bytes, 0, items, 0, bytes.Length);
                Array.Copy(items, 0, allItems, i * maxLen, maxLen);
            }
            // Remainder
            if (remainder > 0)
            {
                items = new int[remainder];
                bytes = binaryReader.ReadBytes(remainder * sizeof(int));
                Buffer.BlockCopy(bytes, 0, items, 0, bytes.Length);
                Array.Copy(items, 0, allItems, numLoops * maxLen, remainder);
            }
            // Read parenthesis
            binaryReader.ReadChar();
            //
            return allItems;
        }
        static private int[] ReadIntArrayAsOnePart(BinaryReader binaryReader, int numOfItems)
        {
            // Read parenthesis
            binaryReader.ReadChar();
            // Read data
            byte[] bytes = new byte[numOfItems * sizeof(int)];
            bytes = binaryReader.ReadBytes(bytes.Length);
            int[] items = new int[numOfItems];
            Buffer.BlockCopy(bytes, 0, items, 0, bytes.Length);
            // Read parenthesis
            binaryReader.ReadChar();
            //
            return items;
        }
        static private double[] ReadDoubleArray(BinaryReader binaryReader, int numOfItems)
        {
            // Read parenthesis
            binaryReader.ReadChar();
            // Read data
            byte[] bytes = new byte[numOfItems * sizeof(double)];
            bytes = binaryReader.ReadBytes(bytes.Length);
            double[] items = new double[numOfItems];
            Buffer.BlockCopy(bytes, 0, items, 0, bytes.Length);
            // Read parenthesis
            binaryReader.ReadChar();
            //
            return items;
        }
    }
}
