Imports System.Globalization
Imports System.IO
Imports System.Threading
Imports Pilz.S3DFileParser.Exceptions

Namespace ObjModule

    Public Class ObjFile

        Public Shared Function FromFile(FileName As String, LoadMaterials As Boolean, UpAxis As UpAxis) As Object3D
            Dim curThread As Thread = Thread.CurrentThread
            Dim curCultInfo As CultureInfo = curThread.CurrentCulture
            Dim newCultInfo As New CultureInfo(curCultInfo.Name)
            newCultInfo.NumberFormat.NumberDecimalSeparator = "."
            newCultInfo.NumberFormat.PercentDecimalSeparator = "."
            newCultInfo.NumberFormat.CurrencyDecimalSeparator = "."
            newCultInfo.NumberFormat.NumberGroupSeparator = ","
            newCultInfo.NumberFormat.PercentGroupSeparator = ","
            newCultInfo.NumberFormat.CurrencyGroupSeparator = ","
            curThread.CurrentCulture = newCultInfo

            Dim newObj As New Object3D
            Dim newMesh As New Mesh
            Dim curObjPath As String = Path.GetDirectoryName(FileName)

            Dim mtllibs As New Dictionary(Of String, MaterialLib)

            Dim curMaterialLib As MaterialLib = Nothing
            Dim curMaterial As Material = Nothing

            Dim srObj As New StreamReader(FileName, Text.Encoding.ASCII)
            Dim line As String = ""

            Do Until srObj.EndOfStream
                line = srObj.ReadLine.Trim

                If line <> "" Then

                    Select Case True
                        Case line.StartsWith("mtllib ")
                            Dim name As String = line.Substring(7)

                            If Not mtllibs.ContainsKey(name) Then
                                Dim mtlfile As String = Path.Combine(curObjPath, name)
                                If Not File.Exists(mtlfile) Then Throw New MaterialException("Material Library not found!")

                                Dim newmtl As New MaterialLib
                                newmtl.FromFile(mtlfile, LoadMaterials)
                                mtllibs.Add(name, newmtl)
                                curMaterialLib = newmtl

                                For Each kvp As KeyValuePair(Of String, Material) In curMaterialLib.Materials
                                    If Not newObj.Materials.ContainsKey(kvp.Key) Then newObj.Materials.Add(kvp.Key, kvp.Value)
                                Next
                            Else
                                curMaterialLib = mtllibs(name)
                            End If

                        Case line.StartsWith("usemtl ")
                            curMaterial = curMaterialLib.Materials(line.Substring(7))

                        Case line.StartsWith("v ")
                            If line.Contains("nan") Then line = line.Replace("nan", "0")
                            Dim splitXYZ() As String = line.Substring(2).Split(" "c)

                            Dim tX As Double = Convert.ToDouble(splitXYZ(0))
                            Dim tY As Double = Convert.ToDouble(splitXYZ(1))
                            Dim tZ As Double = Convert.ToDouble(splitXYZ(2))

                            Dim v As New Vertex
                            Select Case UpAxis
                                Case UpAxis.Y
                                    v.X = tX
                                    v.Y = tY
                                    v.Z = tZ
                                Case UpAxis.Z
                                    v.X = tY
                                    v.Y = tZ
                                    v.Z = tX
                            End Select
                            newMesh.Vertices.Add(v)

                        Case line.StartsWith("vt ")
                            Dim uvstr() As String = line.Substring(3).Split(" "c)
                            Dim uv As New UV With {
                            .U = Convert.ToSingle(uvstr(0)),
                            .V = Convert.ToSingle(uvstr(1))}
                            newMesh.UVs.Add(uv)

                        Case line.StartsWith("vn ")
                            Dim splitXYZ() As String = line.Substring(3).Split(" "c)

                            Dim tX As Single = Convert.ToSingle(splitXYZ(0))
                            Dim tY As Single = Convert.ToSingle(splitXYZ(1))
                            Dim tZ As Single = Convert.ToSingle(splitXYZ(2))

                            Dim n As New Normal
                            Select Case UpAxis
                                Case UpAxis.Y
                                    n.X = tX
                                    n.Y = tY
                                    n.Z = tZ
                                Case UpAxis.Z
                                    n.X = tZ
                                    n.Y = tY
                                    n.Z = tX
                            End Select
                            newMesh.Normals.Add(n)

                        Case line.StartsWith("vc ")
                            Dim splitRGB() As String = line.Substring(3).Split(" "c)

                            Dim tX As Single = Convert.ToSingle(splitRGB(0))
                            Dim tY As Single = Convert.ToSingle(splitRGB(1))
                            Dim tZ As Single = Convert.ToSingle(splitRGB(2))

                            Dim vc As New VertexColor
                            Select Case UpAxis
                                Case UpAxis.Y
                                    vc.R = tX
                                    vc.G = tY
                                    vc.B = tZ
                                Case UpAxis.Z
                                    vc.R = tY
                                    vc.G = tZ
                                    vc.B = tX
                            End Select
                            newMesh.VertexColors.Add(vc)

                        Case line.StartsWith("f ")
                            Dim tri As New Face With {.Material = curMaterial}

                            For Each xyz As String In line.Substring(2).Split(" "c)
                                xyz = xyz.Trim
                                If xyz = "" Then Continue For

                                Dim splitsub() As String = Nothing
                                Dim p As New Point

                                Select Case True
                                    Case xyz.Contains("/")
                                        splitsub = xyz.Split("/"c)
                                    Case xyz.Contains("\")
                                        splitsub = xyz.Split("\"c)
                                    Case Else
                                        splitsub = {0, 0, 0}
                                End Select

                                Dim v1 As String = splitsub(0)
                                Dim v2 As String = splitsub(1)
                                Dim v3 As String = splitsub(2)

                                If v1 <> "" Then
                                    p.Vertex = newMesh.Vertices(Convert.ToInt32(v1) - 1)
                                End If

                                If v2 <> "" Then
                                    p.UV = newMesh.UVs(Convert.ToInt32(v2) - 1)
                                Else
                                    Dim newUV As New UV With {.U = 0, .V = 0}
                                    p.UV = newUV
                                    newMesh.UVs.Add(newUV)
                                End If

                                If v3 <> "" Then
                                    p.Normal = newMesh.Normals(Convert.ToInt32(v3) - 1)
                                End If

                                If splitsub.Count > 3 Then
                                    Dim v4 As String = splitsub(3)
                                    If v4 <> "" Then p.VertexColor = newMesh.VertexColors(Convert.ToInt32(v4) - 1)
                                End If

                                tri.Points.Add(p)
                            Next

                            newMesh.Faces.Add(tri)

                    End Select

                End If

            Loop
            newObj.Meshes.Add(newMesh)

            curThread.CurrentCulture = curCultInfo

            srObj.Close()
            Return newObj
        End Function

        Public Shared Sub ToFile(FileName As String, obj As Object3D)
            Dim fs As New FileStream(FileName, FileMode.Create, FileAccess.ReadWrite)
            Dim sw As New StreamWriter(fs, Text.Encoding.ASCII)

            If obj.Materials.Count > 0 Then
                Dim mtlName As String = Path.GetFileNameWithoutExtension(FileName) & ".mtl"
                Dim mtlFile As String = Path.Combine(Path.GetDirectoryName(FileName), mtlName)
                sw.WriteLine($"mtllib {mtlName}")
                MaterialLib.ToFile(mtlFile, obj)
            End If

            Dim curVertCount As Integer = 1
            Dim curUVCount As Integer = 1
            Dim curNormCount As Integer = 1
            Dim curVertColCount As Integer = 1

            For Each m As Mesh In obj.Meshes

                For Each vert As Vertex In m.Vertices
                    sw.WriteLine($"v {vert.X.ToString.Replace(",", ".")} {vert.Y.ToString.Replace(",", ".")} {vert.Z.ToString.Replace(",", ".")}")
                Next

                For Each uv As UV In m.UVs
                    sw.WriteLine($"vt {uv.U.ToString.Replace(",", ".")} {uv.V.ToString.Replace(",", ".")}")
                Next

                For Each norm As Normal In m.Normals
                    sw.WriteLine($"vn {norm.X.ToString.Replace(",", ".")} {norm.Y.ToString.Replace(",", ".")} {norm.Z.ToString.Replace(",", ".")}")
                Next

                For Each vertcol As VertexColor In m.VertexColors
                    sw.WriteLine($"vc {vertcol.R.ToString.Replace(",", ".")} {vertcol.G.ToString.Replace(",", ".")} {vertcol.B.ToString.Replace(",", ".")}")
                Next

                Dim curMtl As Material = Nothing

                For Each f As Face In m.Faces
                    If curMtl IsNot f.Material Then
                        curMtl = f.Material
                        sw.WriteLine($"usemtl _{GetIndexOfMaterialInList(obj, curMtl)}")
                    End If

                    sw.Write("f")

                    For Each p As Point In f.Points
                        sw.Write(" ")
                        sw.Write(curVertCount + m.Vertices.IndexOf(p.Vertex))

                        sw.Write("/")
                        If p.UV IsNot Nothing Then sw.Write(curUVCount + m.UVs.IndexOf(p.UV))

                        sw.Write("/")
                        If p.Normal IsNot Nothing Then sw.Write(curNormCount + m.Normals.IndexOf(p.Normal))

                        If m.VertexColors.Count > 0 Then
                            sw.Write("/")
                            If p.VertexColor IsNot Nothing Then sw.Write(curVertColCount + m.VertexColors.IndexOf(p.VertexColor))
                        End If
                    Next

                    sw.WriteLine()
                Next

                curVertCount += m.Vertices.Count
                curUVCount += m.UVs.Count
                curNormCount += m.Normals.Count
                curVertColCount += m.VertexColors.Count
            Next

            sw.Flush()
            fs.Close()
        End Sub

        Public Shared Function GetIndexOfMaterialInList(obj As Object3D, matToFind As Material) As Integer
            For Index As Integer = 0 To obj.Materials.Count - 1
                If obj.Materials.ElementAt(Index).Value.Equals(matToFind) Then
                    Return Index
                End If
            Next
            Return -1
        End Function

    End Class

    Public Class MaterialLib

        Public ReadOnly Property Materials As New Dictionary(Of String, Material)
        Private ReadOnly LoadedImages As New Dictionary(Of String, Image)

        Public Sub FromFile(fileName As String, LoadMaterials As Boolean)
            LoadedImages.Clear()

            Dim curMatLibPath As String = Path.GetDirectoryName(fileName)

            Dim curMat As Material = Nothing
            Dim curName As String = ""

            Dim srMtl As New StreamReader(fileName, Text.Encoding.ASCII)
            Dim line As String = ""

            Do Until srMtl.EndOfStream
                line = srMtl.ReadLine

                Select Case True
                    Case line.StartsWith("newmtl ")
                        curMat = New Material
                        curName = line.Substring(7)
                        Materials.Add(curName, curMat)

                    Case line.ToLower.StartsWith("kd ")
                        Dim splitColor() As String = line.Substring(3).Split(" "c)
                        Dim col As Color = Color.FromArgb(
                            Convert.ToSingle(Math.Round(255 * splitColor(0))),
                            Convert.ToSingle(Math.Round(255 * splitColor(1))),
                            Convert.ToSingle(Math.Round(255 * splitColor(2))))
                        curMat.Color = col

                    Case line.ToLower.StartsWith("d ")
                        curMat.Opacity = Convert.ToSingle(line.Substring(2))

                    Case line.ToLower.StartsWith("tr ")
                        curMat.Opacity = 1 - Convert.ToSingle(line.Substring(2))

                    Case line.ToLower.StartsWith("map_kd ")
                        If LoadMaterials Then
                            Dim mtlpath As String = line.Substring(7)
                            Dim combipath As String = Path.Combine(curMatLibPath, line.Substring(7))
                            Dim imgfile As String

                            If File.Exists(combipath) Then
                                imgfile = combipath
                            ElseIf File.Exists(line.Substring(7)) Then
                                imgfile = mtlpath
                            Else
                                imgfile = ""
                            End If

                            If imgfile <> "" Then
                                If LoadedImages.ContainsKey(imgfile) Then
                                    curMat.Image = LoadedImages(imgfile)
                                Else
                                    Dim fs As New FileStream(imgfile, FileMode.Open, FileAccess.Read)
                                    curMat.Image = Image.FromStream(fs)
                                    fs.Close()
                                    Dim imgExists As Boolean = False
                                    For Each kvp In LoadedImages
                                        If Not imgExists AndAlso IsTheSameAs(kvp.Value, curMat.Image) Then
                                            curMat.Image = kvp.Value
                                            imgExists = True
                                        End If
                                    Next
                                    If Not imgExists Then
                                        LoadedImages.Add(imgfile, curMat.Image)
                                    End If
                                End If
                            End If
                        End If

                End Select
            Loop

            srMtl.Close()
        End Sub

        Public Shared Sub ToFile(fileName As String, obj As Object3D)
            Dim fs As New FileStream(fileName, FileMode.Create, FileAccess.ReadWrite)
            Dim sw As New StreamWriter(fs, Text.Encoding.ASCII)
            Dim imgDirName As String = Path.GetFileNameWithoutExtension(fileName)
            Dim imgDirFull As String = Path.Combine(Path.GetDirectoryName(fileName), Path.GetFileNameWithoutExtension(fileName))

            For Each kvp As KeyValuePair(Of String, Material) In obj.Materials
                Dim mat As Material = kvp.Value
                Dim name As String = "_" & ObjFile.GetIndexOfMaterialInList(obj, mat)
                sw.WriteLine($"newmtl {name}")

                If mat.Color IsNot Nothing Then
                    sw.WriteLine($"kd {(mat.Color.Value.R / 255).ToString.Replace(",", ".")} {(mat.Color.Value.G / 255).ToString.Replace(",", ".")} {(mat.Color.Value.B / 255).ToString.Replace(",", ".")}")
                End If

                If mat.Opacity IsNot Nothing Then
                    sw.WriteLine($"d {mat.Opacity.Value.ToString.Replace(",", ".")}")
                End If

                If mat.Image IsNot Nothing Then
                    Dim imgFile As String = name & ".png"

                    If Not Directory.Exists(imgDirFull) Then Directory.CreateDirectory(imgDirFull)
                    mat.Image.Save(Path.Combine(imgDirFull, imgFile), Imaging.ImageFormat.Png)

                    sw.WriteLine($"map_kd {Path.Combine(imgDirName, imgFile)}")
                End If
            Next

            sw.Flush()
            fs.Close()
        End Sub

    End Class

End Namespace