using Godot;
using System;
using System.Collections.Generic;
using System.Linq;

public partial class PlanetGenerator : Node3D
{
    [Export] FastNoiseLite noise;
    [Export] FastNoiseLite hfnoise;
    [Export] private int _plateCount = 55;
    [Export] private float oceanPercent = 0.6f;

    Dictionary<int, List<int>> plateVerticeCandidates = new Dictionary<int, List<int>>();
    Dictionary<int, PlateDataOld> plates = new Dictionary<int, PlateDataOld>();
    private int stage = -1;
    
    MeshDataTool mdt = new MeshDataTool();

    public class PlateDataOld(List<VertexDataOld> vertices = null, bool isLand = false, Vector3? dir = null, Color color = new())
    {
        public List<VertexDataOld> Vertices = vertices ?? new List<VertexDataOld>();
        public Color Color = color;
        public bool IsLand = isLand;
        public Vector3? Dir = dir;
    }

    public class VertexDataOld(int index = 0, float stress = 0f, bool isEdge = false)
    {
        public int Index = index;
        public float Stress = stress;
        public bool IsEdge = isEdge;
        public bool StageProcessed = false;
        public float Height = 0f;
    }
    public override void _Ready()
    {
        var MeshInstance = GetNode<MeshInstance3D>("./Icosphere");
        if (MeshInstance.GetSurfaceOverrideMaterial(0) is ShaderMaterial shaderMaterial)
        {
            shaderMaterial.SetShaderParameter("mode", 1);
        }
    }

    public void InitializeGeneration()
    {
        plateVerticeCandidates = new();
        plates = new();
        var MeshInstance = GetNode<MeshInstance3D>("./Icosphere");
        if (MeshInstance.Mesh is ArrayMesh mesh)
        {
            mdt.CreateFromSurface(mesh, 0);
            GD.Print($"MDTVertices: '{mdt.GetVertexCount()}', MDTFaces: '{mdt.GetFaceCount()}', MESHFaces: '{MeshInstance.Mesh.GetFaces().Length}'");
            for (int i = 0; i < mdt.GetVertexCount(); i++)
            {
                mdt.SetVertexColor(i, Colors.Black);
            }
            List<bool> isLandList = new List<bool>();
            isLandList.AddRange(Enumerable.Repeat(true, (int)(_plateCount * oceanPercent)));
            isLandList.AddRange(Enumerable.Repeat(false, (int)(_plateCount * 1f - oceanPercent)));
            isLandList = isLandList.OrderBy(l => Guid.NewGuid()).ToList();
            for (int i = 0; i < _plateCount; i++)
            {
                bool isLand = isLandList[i];
                var vertexIndex = Random.Shared.Next(0, mdt.GetVertexCount());

                var color = isLand ? new Color(
                    0.2f,
                    1f,
                    0.2f
                    ) : new Color(
                    0.2f,
                    0.2f,
                    1f
                    );
                color.ToHsv(out float h, out float s, out float v);
                h += RandF(-0.05f, 0.05f);
                s += RandF(-0.2f, 0.2f);
                v += RandF(-0.3f, 0.3f);
                color = Color.FromHsv(h, s, v);
                plateVerticeCandidates.Add(i, GetNeighboringVertices(vertexIndex).ToList());
                var plateVel = new Vector3(Random.Shared.NextSingle() * 2f - 1f ,Random.Shared.NextSingle() * 2f - 1f ,Random.Shared.NextSingle() * 2f - 1f);
                plateVel = plateVel.Normalized() * Random.Shared.NextSingle();
                plates.Add(i, new PlateDataOld([new VertexDataOld(vertexIndex)], isLand, plateVel, color));
                mdt.SetVertexColor(vertexIndex, color);
            }
            
            mesh.ClearSurfaces();
            mdt.CommitToSurface(mesh);
        }
    }

    private float RandF(float min, float max)
    {
        return min + (max - min) * Random.Shared.NextSingle();
    }
    
    private int iterations = 1;
    public override void _Process(double delta)
    {
        if (Input.IsActionJustPressed("spacebar"))
        {
            InitializeGeneration();
            stage = 0;
            var MeshInstance = GetNode<MeshInstance3D>("./Icosphere");
            if (MeshInstance.GetSurfaceOverrideMaterial(0) is ShaderMaterial shaderMaterial)
            {
                shaderMaterial.SetShaderParameter("mode", 1);
            }
        }

        if (GetNode<MeshInstance3D>("./Icosphere").Mesh is ArrayMesh mesh)
        {
            switch (stage)
            {
                case 0: // Gen initial tectonic plates
                    if (plateVerticeCandidates.Count == 0)
                        stage = 1;


                    List<int> toRemove = [];
                    foreach ((int plateIndex, List<int> verticeCanditates) in plateVerticeCandidates)
                    {
                        if (verticeCanditates.Count == 0)
                            toRemove.Add(plateIndex);
                        int max = Random.Shared.Next(1, (int)(2 + verticeCanditates.Count / 4f));
                        for (int i = 0; i < max; i++)
                        {
                            iterations++;
                            ExpandRandomly(verticeCanditates, plateIndex);
                        }

                        plateVerticeCandidates[plateIndex] = verticeCanditates.Distinct().ToList();
                    }

                    foreach (int index in toRemove)
                        plateVerticeCandidates.Remove(index);
                    mesh.ClearSurfaces();
                    mdt.CommitToSurface(mesh);

                    break;
                case 1:
                    foreach ((int plateIndex, PlateDataOld data) in plates)
                    {
                        foreach (VertexDataOld vertex in data.Vertices.Where(v => !v.StageProcessed).Take(50))
                        {
                            if (IsEdge(vertex.Index, plateIndex))
                            {
                                vertex.IsEdge = true;
                            }
                            vertex.StageProcessed = true;
                            mdt.SetVertexColor(vertex.Index, vertex.IsEdge ? Colors.Black : Colors.White);
                        }
                    }
                    mesh.ClearSurfaces();
                    mdt.CommitToSurface(mesh);
                    if (plates.Values.SelectMany(val => val.Vertices).All(v => v.StageProcessed))
                    {
                        foreach ((int plateIndex, PlateDataOld data) in plates)
                        {
                            foreach (VertexDataOld vertex in data.Vertices)
                            {
                                vertex.StageProcessed = false;
                            }
                        }
                        stage = 2;
                    }
                    break;
                case 2:
                    foreach ((int plateIndex, PlateDataOld data) in plates)
                    {
                        foreach (VertexDataOld vertex in data.Vertices.Where(v => !v.StageProcessed && v.IsEdge).Take(10))
                        {
                            vertex.Stress = GetStress(vertex.Index, plateIndex);
                            vertex.StageProcessed = true;
                            mdt.SetVertexColor(vertex.Index, vertex.Stress > 0f ? Colors.Red : Colors.Blue);
                        }
                    }
                    mesh.ClearSurfaces();
                    mdt.CommitToSurface(mesh);
                    if (plates.Values.SelectMany(val => val.Vertices).Where(v => v.IsEdge).All(v => v.StageProcessed))
                    {
                        foreach ((int plateIndex, PlateDataOld data) in plates)
                        {
                            foreach (VertexDataOld vertex in data.Vertices)
                            {
                                vertex.StageProcessed = false;
                            }
                        }
                        stage = 3;
                        var MeshInstance = GetNode<MeshInstance3D>("./Icosphere");
                        if (MeshInstance.GetSurfaceOverrideMaterial(0) is ShaderMaterial shaderMaterial)
                        {
                            shaderMaterial.SetShaderParameter("mode", 2);
                        }
                    }

                    break;
                case 3:
                    foreach ((int plateIndex, PlateDataOld data) in plates)
                    {
                        foreach (VertexDataOld vertex in data.Vertices.Where(v => !v.StageProcessed).Take(10))
                        {
                            vertex.Height = GetHeight(vertex, plateIndex);
                            vertex.StageProcessed = true;
                            mdt.SetVertexColor(vertex.Index, new Color(vertex.Height, vertex.Height, vertex.Height));
                        }
                    }
                    mesh.ClearSurfaces();
                    mdt.CommitToSurface(mesh);
                    if (plates.Values.SelectMany(val => val.Vertices).All(v => v.StageProcessed))
                    {
                        foreach ((int plateIndex, PlateDataOld data) in plates)
                        {
                            foreach (VertexDataOld vertex in data.Vertices)
                            {
                                vertex.StageProcessed = false;
                            }
                        }
                        stage = 4;
                    }
                    break;
                default:
                    break;
            }
        }
    }

    public void ColorFace(int faceIndex, Color color)
    {
        mdt.SetVertexColor(mdt.GetFaceVertex(faceIndex, 0), color);
        mdt.SetVertexColor(mdt.GetFaceVertex(faceIndex, 1), color);
        mdt.SetVertexColor(mdt.GetFaceVertex(faceIndex, 2), color);
    }

    public bool IsEdge(int vertex, int plateIndex)
    {
        var neighbours = GetNeighboringVertices(vertex, false);
        foreach (var neighbour in neighbours)
        {
            if (plates[plateIndex].Vertices.All(v => v.Index != neighbour))
                return true;
        }
        return false;
    }
    public float GetStress(int vertex, int plateIndex)
    {
        PlateDataOld plate = plates[plateIndex];
        var neighbours = GetNeighboringVertices(vertex, false);
        float stress = 0f;
        
        foreach ((int otherPlateIndex, PlateDataOld plateData) in plates.Where(p => p.Key != plateIndex))
        {
            if (plateData.Vertices.Where(v => v.IsEdge).Any(v => neighbours.Contains(v.Index)))
            {
                var a = plate.Dir ?? Vector3.Zero;
                var b = plateData.Dir ?? Vector3.Zero;
                stress += a.Dot(b);
            }
        }
        return stress;
    }

    public float GetHeight(VertexDataOld vertex, int plateIndex)
    {
        PlateDataOld plate = plates[plateIndex];
        float height = 0.5f;

        if (plate.IsLand)
        {
            height += Mathf.Abs(vertex.Stress * 0.25f);
            height += noise.GetNoise3Dv(mdt.GetVertex(vertex.Index)) * 0.15f;
            height += hfnoise.GetNoise3Dv(mdt.GetVertex(vertex.Index)) * 0.05f;
        }
        else
        {
            height -= Mathf.Abs(vertex.Stress * 0.25f);
            height += noise.GetNoise3Dv(mdt.GetVertex(vertex.Index)) * 0.15f;
            height += hfnoise.GetNoise3Dv(mdt.GetVertex(vertex.Index)) * 0.05f;
        }

        return height;
    }
    public void ExpandRandomly(List<int> vertexCandidate, int plateIndex)
    {
        bool looping = true;
        var index = 0;
        while (looping)
        {
            if (vertexCandidate.Count == 0)
            {
                looping = false;
                continue;
            }
            index = Random.Shared.Next(vertexCandidate.Count);
            var vertex = vertexCandidate[index];
            if (mdt.GetVertexColor(vertex) != Colors.Black)
            {
                vertexCandidate.RemoveAt(index);
                continue;
            }
            mdt.SetVertexColor(vertex, plates[plateIndex].Color);
            plates[plateIndex].Vertices.Add(new VertexDataOld(vertex));
            vertexCandidate.RemoveAt(index);
            var neighbours = GetNeighboringVertices(vertex).ToList();
            vertexCandidate.AddRange(neighbours);
            looping = false;
        }
    }

    public List<int> GetNeighboringVertices(int vertex, bool blackOnly = true)
    {
        var verts = mdt.GetVertexEdges(vertex).AsEnumerable().SelectMany<int, int>(edge => [mdt.GetEdgeVertex(edge, 0), mdt.GetEdgeVertex(edge, 1)]).Distinct().Where(v => v != vertex);
        if (!blackOnly)
            return verts.ToList();
        return verts.Where(v => mdt.GetVertexColor(v) == Colors.Black).ToList();
    }
}
