using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEditor;
using UnityEditor.SceneManagement;

namespace Tinta.Editor
{
    // Original geometry, authored parametrically in Unity. One unit = one metre.
    public static class HarborAssets
    {
        const string Root = "Assets/Tinta";
        static readonly Dictionary<string, Mesh> Meshes = new Dictionary<string, Mesh>();
        static readonly Dictionary<string, Material> Mats = new Dictionary<string, Material>();
        static string Output => Path.GetFullPath("../artifacts");
        static Color C(string hex) { ColorUtility.TryParseHtmlString("#" + hex, out var c); return c; }
        static Vector3 V(float x, float y, float z) => new Vector3(x,y,z);

        [MenuItem("Tinta/Gerar assets e cena Oficina da Doca")]
        public static void Generate()
        {
            foreach(var d in new[]{"Meshes","Materials","Textures","Prefabs","Scenes"}) Directory.CreateDirectory(Root+"/"+d);
            Directory.CreateDirectory(Output); AssetDatabase.Refresh(); Meshes.Clear(); Mats.Clear();
            var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
            PlayerSettings.colorSpace = ColorSpace.Linear;
            PlayerSettings.companyName = "Tinta"; PlayerSettings.productName = "Tinta — Oficina da Doca";
            GraphicsSettings.defaultRenderPipeline = null;
            QualitySettings.renderPipeline = null; QualitySettings.shadows = ShadowQuality.All;
            QualitySettings.shadowResolution = ShadowResolution.High; QualitySettings.shadowDistance=100;
            QualitySettings.antiAliasing=4;
            Materials();
            var prototypes = new Dictionary<string, GameObject>();
            prototypes["DeckTile"] = Tile();
            prototypes["CargoCase"] = Case();
            prototypes["PaintDrum"] = Drum();
            prototypes["PumpStation"] = Pump();
            prototypes["ConcreteBarrier"] = Barrier();
            prototypes["WorkLight"] = WorkLight();
            prototypes["PainterAzure"] = Painter("PainterAzure", "Azure");
            prototypes["PainterCoral"] = Painter("PainterCoral", "Coral");
            prototypes["HarborEnvironment"] = Environment();
            foreach(var pair in prototypes)
            {
                SaveMeshes(pair.Value);
                PrefabUtility.SaveAsPrefabAsset(pair.Value, Root+"/Prefabs/"+pair.Key+".prefab");
                NativeExport.Write(pair.Value, Output+"/"+pair.Key+".json");
            }
            foreach(var go in prototypes.Values) UnityEngine.Object.DestroyImmediate(go);
            var env = Spawn("HarborEnvironment", V(0,0,0));
            var deck = new GameObject("Deck — 11 x 20");
            for(int z=0;z<20;z++) for(int x=0;x<11;x++)
            {
                var t=Spawn("DeckTile", V(x+.5f,0,z+.5f)); t.transform.SetParent(deck.transform,true);
                bool a = (z>11 && x==3) || (z==12 && x>=3 && x<=8) || (x==8 && z>=12 && z<=17) || (z==17 && x>=5 && x<=8);
                bool b = (x==7 && z<9 && z>2) || (z==8 && x>=2 && x<=7) || (x==2 && z>=5 && z<=8);
                if(a||b)t.transform.Find("PaintableSurface").GetComponent<Renderer>().sharedMaterial=Mats[a?"PaintAzure":"PaintCoral"];
            }
            foreach(var p in new[]{V(2.5f,0,3.5f),V(8.5f,0,3.5f),V(5.5f,0,6.5f),V(1.5f,0,7.5f),V(9.5f,0,7.5f),V(2.5f,0,16.5f),V(8.5f,0,16.5f),V(5.5f,0,13.5f),V(1.5f,0,12.5f),V(9.5f,0,12.5f)}) Spawn("CargoCase",p);
            Spawn("PainterAzure",V(5.5f,.18f,17.5f)).transform.rotation=Quaternion.Euler(0,160,0);
            Spawn("PainterCoral",V(2.5f,.18f,5.5f)).transform.rotation=Quaternion.Euler(0,25,0);
            SetupLighting();
            var camera = new GameObject("Camera — arena").AddComponent<Camera>();
            camera.tag="MainCamera"; camera.transform.position=V(27,29,37);camera.transform.LookAt(V(5.5f,0,10));
            camera.fieldOfView=39; camera.nearClipPlane=.1f;camera.farClipPlane=160;
            camera.clearFlags=CameraClearFlags.SolidColor;camera.backgroundColor=C("15232c");camera.allowHDR=true;
            EditorSceneManager.SaveScene(scene,Root+"/Scenes/OficinaDaDoca.unity");
            AssetDatabase.SaveAssets();
            if(SystemInfo.graphicsDeviceType!=GraphicsDeviceType.Null)
            {
                Capture(camera,Output+"/arena-unity.png",1600,1200);
                foreach(var r in UnityEngine.Object.FindObjectsByType<Renderer>(FindObjectsSortMode.None))r.enabled=false;
                var sample=new GameObject("Showcase");
                for(int z=0;z<7;z++)for(int x=0;x<10;x++){var tile=Spawn("DeckTile",V(x-4.5f,-.18f,z-3));tile.transform.SetParent(sample.transform);}
                Spawn("PainterAzure",V(-1.6f,0,-.4f)).transform.rotation=Quaternion.Euler(0,210,0);
                Spawn("PainterCoral",V(.1f,0,-.4f)).transform.rotation=Quaternion.Euler(0,195,0);
                Spawn("CargoCase",V(1.9f,0,-.8f)).transform.rotation=Quaternion.Euler(0,20,0);
                Spawn("PaintDrum",V(3,0,.25f));Spawn("PumpStation",V(.2f,0,1.55f));
                Spawn("ConcreteBarrier",V(-2.4f,0,1.9f));Spawn("WorkLight",V(2.4f,0,1.9f));
                camera.transform.position=V(9,8,-12);camera.transform.LookAt(V(0,.6f,.4f));camera.fieldOfView=33;
                Capture(camera,Output+"/assets-unity.png",1600,1100);
            }
            Package();
            Debug.Log("TINTA_ASSETS_OK: "+prototypes.Count+" prefabs generated, exported and scene saved.");
        }
        public static void Package()
        {
            // Prune previous generated mesh revisions, keeping every prefab/scene dependency.
            var roots=Directory.GetFiles("Assets","*.prefab",SearchOption.AllDirectories).Concat(Directory.GetFiles("Assets","*.unity",SearchOption.AllDirectories)).ToArray();
            var keep=new HashSet<string>(AssetDatabase.GetDependencies(roots,true));
            foreach(var path in Directory.GetFiles(Root+"/Meshes","*.asset"))if(!keep.Contains(path))AssetDatabase.DeleteAsset(path);
            AssetDatabase.SaveAssets();
            AssetDatabase.ExportPackage(Root,Output+"/Tinta-OficinaDaDoca.unitypackage",ExportPackageOptions.Recurse);
        }
        static GameObject Spawn(string name,Vector3 p)
        {
            var g=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Root+"/Prefabs/"+name+".prefab"));g.transform.position=p;return g;
        }
        static void Materials()
        {
            Mat("Concrete","7d8984",0,.8f,true);Mat("Deck","65736f",.08f,.69f,true);
            Mat("Steel","465b5e",.72f,.39f,true);Mat("Edge","8eaaa8",.8f,.31f);
            Mat("Dark","202f36",.3f,.68f,true);Mat("Rubber","172328",0,.84f,true);
            Mat("Ivory","d5d3b9",.12f,.43f,true);Mat("Teal","397b78",.45f,.42f,true);
            Mat("Amber","dcaa50",.18f,.5f,true);Mat("Rust","8e5640",.55f,.8f,true);
            Mat("Azure","347cd3",.1f,.57f,true);Mat("Coral","df6049",.1f,.57f,true);
            Mat("PaintAzure","258bf5",.12f,.27f);Mat("PaintCoral","f56545",.12f,.27f);
            Mat("Visor","102e39",.72f,.13f);Mat("Skin","bb8d66",0,.7f);
            Mat("Lamp","ffddb0",0,.3f,false,"ffcd83",2.2f);Mat("Indicator","5be6c3",.1f,.3f,false,"39e2b9",1.6f);
        }
        static Material Mat(string name,string col,float metal,float rough,bool texture=false,string emission=null,float strength=1)
        {
            var m=new Material(Shader.Find("Standard")){name=name,color=C(col)};
            m.SetFloat("_Metallic",metal);m.SetFloat("_Glossiness",1-rough);m.enableInstancing=true;
            if(texture)m.mainTexture=SurfaceTexture(name);
            if(emission!=null){m.EnableKeyword("_EMISSION");m.SetColor("_EmissionColor",C(emission)*strength);}
            var path=Root+"/Materials/"+name+".mat";if(File.Exists(path))AssetDatabase.DeleteAsset(path);AssetDatabase.CreateAsset(m,path);Mats[name]=m;return m;
        }
        static Texture2D SurfaceTexture(string name)
        {
            int n=256;var t=new Texture2D(n,n,TextureFormat.RGB24,false);var pix=new Color[n*n];
            var rng=new System.Random(1729);bool fabric=name=="Azure"||name=="Coral";bool rough=name=="Concrete"||name=="Deck";
            for(int y=0;y<n;y++)for(int x=0;x<n;x++)
            {
                // Tileable neutral microstructure, created directly by this editor script.
                float a=Mathf.Sin(x*Mathf.PI*2/n)*.5f+.5f,b=Mathf.Cos(y*Mathf.PI*2/n)*.5f+.5f;
                float noise=Mathf.PerlinNoise(a*11+12,b*11+7);float fine=(float)rng.NextDouble();
                float v=rough?.78f+noise*.16f+fine*.06f:.86f+noise*.09f+fine*.05f;
                if(fabric)v=.86f+((x%3==0||y%3==0)?-.05f:.05f)+fine*.04f;
                if(!fabric&&!rough&&y%61==0&&fine>.6f)v-=.13f;
                pix[y*n+x]=new Color(v,v,v,1);
            }
            t.SetPixels(pix);t.Apply();var path=Root+"/Textures/"+name+"_surface.png";File.WriteAllBytes(path,t.EncodeToPNG());UnityEngine.Object.DestroyImmediate(t);
            AssetDatabase.ImportAsset(path);var imp=(TextureImporter)AssetImporter.GetAtPath(path);imp.isReadable=true;imp.wrapMode=TextureWrapMode.Repeat;imp.SaveAndReimport();return AssetDatabase.LoadAssetAtPath<Texture2D>(path);
        }
        static GameObject Group(string name,Transform p=null){var g=new GameObject(name);if(p)g.transform.SetParent(p,false);return g;}
        static GameObject Part(string name,Mesh mesh,string mat,Transform p,Vector3 pos)
        {
            var g=Group(name,p);g.transform.localPosition=pos;g.AddComponent<MeshFilter>().sharedMesh=mesh;
            var r=g.AddComponent<MeshRenderer>();r.sharedMaterial=Mats[mat];r.shadowCastingMode=ShadowCastingMode.On;r.receiveShadows=true;return g;
        }
        static GameObject Box(string name,Vector3 size,Vector3 pos,string mat,Transform p,float bevel=.035f) => Part(name,Bevel(size,bevel),mat,p,pos);
        static GameObject Cylinder(string name,float r,float h,Vector3 pos,string mat,Transform p,int seg=24)
        {float lip=Mathf.Min(.025f,h*.18f,r*.15f);return Part(name,Lathe(new[]{new Vector2(0,-h/2),new Vector2(r*.86f,-h/2),new Vector2(r,-h/2+lip),new Vector2(r,h/2-lip),new Vector2(r*.86f,h/2),new Vector2(0,h/2)},seg),mat,p,pos);}
        static GameObject Ellipsoid(string name,Vector3 size,Vector3 pos,string mat,Transform p)
        {
            var pr=new List<Vector2>();for(int j=0;j<=16;j++){float t=j*Mathf.PI/16;pr.Add(new Vector2(Mathf.Sin(t)*.5f,-Mathf.Cos(t)*.5f));}
            var g=Part(name,Lathe(pr.ToArray(),28),mat,p,pos);g.transform.localScale=size;return g;
        }
        static void Rod(string name,Vector3 a,Vector3 b,float r,string mat,Transform p)
        {var g=Cylinder(name,r,Vector3.Distance(a,b),(a+b)/2,mat,p,16);g.transform.localRotation=Quaternion.FromToRotation(Vector3.up,b-a);}
        static void Bolt(Vector3 p,Transform parent,float r=.025f){Cylinder("Hex bolt",r,.018f,p,"Edge",parent,6);}
        static Mesh Bevel(Vector3 size,float radius)
        {
            string key="Bevel_"+size.ToString("F3")+"_"+radius.ToString("F3");if(Meshes.ContainsKey(key))return Meshes[key];
            var verts=new List<Vector3>();var normals=new List<Vector3>();var uv=new List<Vector2>();var tris=new List<int>();
            var half=size*.5f;radius=Mathf.Min(radius,Mathf.Min(half.x,Mathf.Min(half.y,half.z))*.8f);var inner=half-Vector3.one*radius;
            for(int axis=0;axis<3;axis++)foreach(int sign in new[]{-1,1})
            {
                int u=(axis+1)%3,v=(axis+2)%3;int start=verts.Count;
                float[] us={-half[u],-inner[u],inner[u],half[u]},vs={-half[v],-inner[v],inner[v],half[v]};
                for(int y=0;y<4;y++)for(int x=0;x<4;x++)
                {
                    Vector3 p=Vector3.zero;p[axis]=sign*half[axis];p[u]=us[x];p[v]=vs[y];
                    Vector3 q=V(Mathf.Clamp(p.x,-inner.x,inner.x),Mathf.Clamp(p.y,-inner.y,inner.y),Mathf.Clamp(p.z,-inner.z,inner.z));
                    var n=(p-q).normalized;verts.Add(q+n*radius);normals.Add(n);uv.Add(new Vector2((us[x]+half[u])/size[u],(vs[y]+half[v])/size[v]));
                }
                for(int y=0;y<3;y++)for(int x=0;x<3;x++){int a=start+y*4+x,b=a+1,c=a+4,d=c+1;if(sign>0)tris.AddRange(new[]{a,b,d,a,d,c});else tris.AddRange(new[]{a,d,b,a,c,d});}
            }
            var mesh=new Mesh{name=key};mesh.SetVertices(verts);mesh.SetNormals(normals);mesh.SetUVs(0,uv);mesh.SetTriangles(tris,0);mesh.RecalculateBounds();Meshes[key]=mesh;return mesh;
        }
        static Mesh Lathe(Vector2[] profile,int segments)
        {
            string key="Lathe_"+segments+"_"+string.Join("_",profile.Select(p=>p.ToString("F3")));if(Meshes.ContainsKey(key))return Meshes[key];
            var verts=new List<Vector3>();var uv=new List<Vector2>();var tris=new List<int>();var normals=new List<Vector3>();
            for(int j=0;j<profile.Length;j++)for(int i=0;i<=segments;i++)
            {
                float angle=i*Mathf.PI*2/segments;verts.Add(V(Mathf.Sin(angle)*profile[j].x,profile[j].y,Mathf.Cos(angle)*profile[j].x));uv.Add(new Vector2((float)i/segments,(float)j/(profile.Length-1)));
                var tangent=profile[Mathf.Min(j+1,profile.Length-1)]-profile[Mathf.Max(0,j-1)];
                var normal=V(Mathf.Sin(angle)*tangent.y,-tangent.x,Mathf.Cos(angle)*tangent.y).normalized;
                if(Mathf.Abs(profile[j].x)<.00001f)normal=j<profile.Length/2?Vector3.down:Vector3.up;
                normals.Add(normal);
            }
            for(int j=0;j<profile.Length-1;j++)for(int i=0;i<segments;i++){int a=j*(segments+1)+i,b=a+1,c=a+segments+1,d=c+1;tris.AddRange(new[]{a,b,d,a,d,c});}
            var mesh=new Mesh{name="Turned part"};mesh.SetVertices(verts);mesh.SetUVs(0,uv);mesh.SetTriangles(tris,0);mesh.SetNormals(normals);mesh.RecalculateBounds();Meshes[key]=mesh;return mesh;
        }
        static void SaveMeshes(GameObject g)
        {
            foreach(var f in g.GetComponentsInChildren<MeshFilter>())
            {
                if(AssetDatabase.Contains(f.sharedMesh))continue;
                var path=AssetDatabase.GenerateUniqueAssetPath(Root+"/Meshes/"+g.name+"_"+f.name.Replace("/","-")+".asset");AssetDatabase.CreateAsset(f.sharedMesh,path);
            }
        }
        static void Combine(GameObject g)
        {
            var filters=g.GetComponentsInChildren<MeshFilter>();var grouped=filters.GroupBy(f=>f.GetComponent<Renderer>().sharedMaterial).ToArray();
            foreach(var group in grouped)
            {
                var combines=group.Select(f=>new CombineInstance{mesh=f.sharedMesh,transform=g.transform.worldToLocalMatrix*f.transform.localToWorldMatrix}).ToArray();
                var mesh=new Mesh{name=g.name+"_"+group.Key.name,indexFormat=IndexFormat.UInt32};mesh.CombineMeshes(combines,true,true);
                var obj=Group(group.Key.name,g.transform);obj.AddComponent<MeshFilter>().sharedMesh=mesh;obj.AddComponent<MeshRenderer>().sharedMaterial=group.Key;
            }
            foreach(var f in filters)UnityEngine.Object.DestroyImmediate(f.gameObject);
        }
        static GameObject Tile()
        {
            var g=Group("DeckTile");Box("Carrier",V(.985f,.12f,.985f),V(0,.01f,0),"Dark",g.transform,.012f);
            Box("PaintableSurface",V(.928f,.08f,.928f),V(0,.11f,0),"Deck",g.transform,.018f);
            foreach(float x in new[]{-.411f,.411f})foreach(float z in new[]{-.411f,.411f})Bolt(V(x,.16f,z),g.transform,.018f);
            return g;
        }
        static GameObject Case()
        {
            var g=Group("CargoCase");var p=g.transform;
            Box("Shock resistant shell",V(.84f,.65f,.84f),V(0,.48f,0),"Teal",p,.075f);
            Box("Bottom gasket",V(.88f,.075f,.88f),V(0,.205f,0),"Rubber",p,.025f);
            Box("Reinforced lid",V(.9f,.13f,.9f),V(0,.85f,0),"Ivory",p,.045f);
            Box("Recessed lid panel",V(.59f,.022f,.59f),V(0,.925f,0),"Teal",p,.03f);
            foreach(float x in new[]{-.35f,.35f})foreach(float z in new[]{-.35f,.35f})
            {Box("Corner armor",V(.14f,.62f,.14f),V(x,.5f,z),"Steel",p,.022f);Bolt(V(x,.936f,z),p);}
            foreach(float z in new[]{-.43f,.43f})
            {
                Box("Recessed handle",V(.29f,.11f,.027f),V(0,.61f,z),"Dark",p,.02f);
                Rod("Handle",V(-.1f,.64f,z*1.035f),V(.1f,.64f,z*1.035f),.023f,"Edge",p);
                foreach(float x in new[]{-.28f,.28f}){Box("Steel latch",V(.078f,.2f,.036f),V(x,.76f,z),"Edge",p,.01f);Box("Latch catch",V(.04f,.04f,.044f),V(x,.76f,z*1.03f),"Dark",p,.006f);}
            }
            Box("Identity plate",V(.3f,.14f,.016f),V(.1f,.4f,.429f),"Ivory",p,.006f);
            foreach(float x in new[]{.02f,.06f,.11f,.14f,.19f})Box("Inventory bars",V(.016f,.08f,.008f),V(x,.4f,.44f),"Dark",p,.002f);
            Combine(g);var col=g.AddComponent<BoxCollider>();col.center=V(0,.55f,0);col.size=V(.9f,.8f,.9f);return g;
        }
        static GameObject Drum()
        {
            var g=Group("PaintDrum");var p=g.transform;Cylinder("Enamel drum",.33f,.94f,V(0,.62f,0),"Ivory",p,32);
            foreach(float y in new[]{.19f,.37f,.86f,1.085f})Cylinder("Rolled steel hoop",.347f,.055f,V(0,y,0),"Steel",p,32);
            Cylinder("Colour band",.335f,.22f,V(0,.64f,0),"Amber",p,32);Cylinder("Top",.3f,.025f,V(0,1.1f,0),"Dark",p,32);
            Cylinder("Bung",.06f,.045f,V(.16f,1.12f,0),"Edge",p,8);Combine(g);var collider=g.AddComponent<CapsuleCollider>();collider.center=V(0,.65f,0);collider.radius=.347f;collider.height=1;return g;
        }
        static GameObject Barrier()
        {
            var g=Group("ConcreteBarrier");var p=g.transform;
            Box("Concrete footing",V(2.6f,.25f,.7f),V(0,.14f,0),"Concrete",p,.06f);
            Box("Concrete curb",V(2.5f,.65f,.45f),V(0,.55f,0),"Concrete",p,.07f);
            Box("Rubber coping",V(2.5f,.09f,.46f),V(0,.9f,0),"Dark",p,.025f);
            foreach(float x in new[]{-.96f,.96f}){Box("Reflector",V(.2f,.13f,.035f),V(x,.71f,-.231f),"Amber",p,.008f);Bolt(V(x,.96f,0),p,.035f);}
            foreach(float x in new[]{-.5f,.5f}){Rod("Hoist eye",V(x-.08f,.94f,0),V(x-.08f,1.045f,0),.025f,"Steel",p);Rod("Hoist handle",V(x-.08f,1.045f,0),V(x+.08f,1.045f,0),.025f,"Steel",p);Rod("Hoist eye",V(x+.08f,1.045f,0),V(x+.08f,.94f,0),.025f,"Steel",p);}
            Combine(g);var collider=g.AddComponent<BoxCollider>();collider.size=V(2.6f,1.05f,.7f);collider.center=V(0,.525f,0);return g;
        }
        static GameObject Pump()
        {
            var g=Group("PumpStation");var p=g.transform;Box("Skid",V(1.5f,.18f,.95f),V(0,.12f,0),"Dark",p);
            Box("Pump enclosure",V(.74f,.96f,.75f),V(-.28f,.68f,0),"Teal",p,.075f);
            Box("Control bezel",V(.48f,.26f,.07f),V(-.28f,.94f,-.397f),"Steel",p,.025f);
            Box("Control display",V(.28f,.12f,.018f),V(-.32f,.97f,-.438f),"Visor",p,.01f);
            Box("Status",V(.13f,.018f,.01f),V(-.33f,.98f,-.45f),"Indicator",p,.003f);
            for(int i=0;i<7;i++)Box("Cooling louvre",V(.47f,.032f,.035f),V(-.28f,.37f+i*.054f,-.393f),"Dark",p,.008f);
            Cylinder("Pressure tank",.25f,1.26f,V(.41f,.86f,.04f),"Ivory",p,32);
            foreach(float y in new[]{.38f,1.26f})Cylinder("Tank clamp",.26f,.07f,V(.41f,y,.04f),"Steel",p,32);
            Rod("Outlet",V(.41f,1.52f,.04f),V(.7f,1.52f,.04f),.06f,"Edge",p);
            Cylinder("Valve spindle",.025f,.18f,V(.58f,1.62f,.04f),"Steel",p);
            Cylinder("Valve wheel",.125f,.035f,V(.58f,1.73f,.04f),"Coral",p,24);
            var gauge=Cylinder("Pressure gauge",.11f,.045f,V(.1f,1.28f,-.26f),"Ivory",p,24);gauge.transform.localEulerAngles=V(90,0,0);
            Box("Gauge needle",V(.014f,.12f,.012f),V(.1f,1.3f,-.288f),"Dark",p,.002f).transform.localEulerAngles=V(0,0,35);
            for(int i=0;i<18;i++){float a=i*Mathf.PI*2/18,b=(i+1)*Mathf.PI*2/18;Rod("Coiled hose",V(-.65f,.63f+Mathf.Sin(a)*.3f,Mathf.Cos(a)*.29f),V(-.65f,.63f+Mathf.Sin(b)*.3f,Mathf.Cos(b)*.29f),.036f,"Rubber",p);}
            Combine(g);return g;
        }
        static GameObject WorkLight()
        {
            var g=Group("WorkLight");var p=g.transform;
            Box("Base",V(.62f,.1f,.55f),V(0,.06f,0),"Dark",p);
            Rod("Mast",V(0,.1f,0),V(0,2.8f,0),.052f,"Steel",p);
            Box("Floodlight housing",V(.68f,.36f,.17f),V(0,2.8f,0),"Steel",p,.04f);
            Box("Floodlight lens",V(.57f,.25f,.018f),V(0,2.8f,-.09f),"Lamp",p,.018f);
            for(int i=0;i<4;i++)Box("Lens divider",V(.018f,.25f,.025f),V(-.18f+i*.12f,2.8f,-.101f),"Edge",p,.003f);
            Combine(g);return g;
        }
        static GameObject Painter(string name,string team)
        {
            var g=Group(name);var p=g.transform;
            Ellipsoid("Tailored coverall torso",V(.63f,.72f,.4f),V(0,1.04f,0),team,p);
            Box("Chest armor",V(.44f,.35f,.13f),V(0,1.16f,.172f),"Ivory",p,.06f);
            Box("Chest inset",V(.28f,.17f,.018f),V(0,1.19f,.247f),team,p,.022f);
            Box("Chest marker",V(.12f,.025f,.012f),V(0,1.2f,.26f),"Lamp",p,.004f);
            Box("Utility belt",V(.56f,.11f,.41f),V(0,.82f,0),"Dark",p,.03f);
            Box("Belt buckle",V(.12f,.07f,.025f),V(0,.83f,.218f),"Edge",p,.01f);
            Cylinder("Neck seal",.16f,.12f,V(0,1.41f,0),"Rubber",p);
            Ellipsoid("Helmet shell",V(.57f,.55f,.54f),V(0,1.68f,0),"Ivory",p);
            Ellipsoid("Visor gasket",V(.535f,.27f,.49f),V(0,1.7f,.065f),"Rubber",p);
            Ellipsoid("Smoked panoramic visor",V(.5f,.215f,.37f),V(0,1.716f,.15f),"Visor",p);
            Box("Visor glint",V(.22f,.019f,.025f),V(-.035f,1.773f,.325f),"Edge",p,.008f);
            Box("Helmet team stripe",V(.105f,.025f,.32f),V(0,1.948f,-.015f),team,p,.012f);
            Box("Respirator",V(.22f,.145f,.115f),V(0,1.535f,.242f),"Dark",p,.035f);
            for(int i=0;i<3;i++)Box("Respirator slot",V(.135f,.012f,.014f),V(0,1.501f+i*.035f,.303f),"Edge",p,.003f);
            foreach(float s in new[]{-1f,1f})
            {
                var ear=Cylinder("Helmet hinge",.073f,.034f,V(s*.285f,1.67f,0),team,p,20);ear.transform.localEulerAngles=V(0,0,90);
                var leg=Group(s<0?"LegLeft":"LegRight",p).transform;leg.localPosition=V(s*.155f,.78f,0);
                Ellipsoid("Coverall leg",V(.25f,.56f,.26f),V(0,-.25f,0),team,leg);
                Box("Knee pad",V(.18f,.19f,.075f),V(0,-.31f,.125f),"Dark",leg,.035f);
                Box("Boot",V(.25f,.2f,.38f),V(0,-.65f,.07f),"Rubber",leg,.05f);
                Box("Boot sole",V(.26f,.06f,.4f),V(0,-.741f,.07f),"Steel",leg,.016f);
                var arm=Group(s<0?"ArmLeft":"ArmRight",p).transform;arm.localPosition=V(s*.355f,1.29f,0);
                Ellipsoid("Sleeve",V(.235f,.47f,.25f),V(s*.025f,-.18f,0),team,arm);
                Ellipsoid("Shoulder protection",V(.28f,.23f,.29f),V(0,-.015f,0),"Ivory",arm);
                Cylinder("Cuff",.109f,.075f,V(s*.025f,-.39f,0),"Dark",arm);
                Ellipsoid("Glove",V(.21f,.2f,.215f),V(s*.025f,-.5f,.02f),"Rubber",arm);
            }
            Box("Tank harness",V(.44f,.49f,.12f),V(0,1.08f,-.24f),"Steel",p,.04f);
            Cylinder("Pigment tank",.165f,.66f,V(0,1.14f,-.36f),team,p,28);
            foreach(float y in new[]{.91f,1.34f})Cylinder("Tank straps",.176f,.055f,V(0,y,-.36f),"Dark",p,28);
            Cylinder("Tank valve",.065f,.08f,V(0,1.5f,-.36f),"Edge",p,12);
            var right=p.Find("ArmRight");Rod("Spray grip",V(.04f,-.49f,.05f),V(.04f,-.49f,.34f),.055f,"Edge",right);
            var nozzle=Cylinder("Paint nozzle",.078f,.14f,V(.04f,-.49f,.34f),"Amber",right,20);nozzle.transform.localEulerAngles=V(90,0,0);
            for(int i=0;i<8;i++){float t=i/8f,q=(i+1)/8f;Rod("Paint feed hose",V(.12f+t*.3f,1.12f-Mathf.Sin(t*Mathf.PI)*.46f,-.35f+t*.38f),V(.12f+q*.3f,1.12f-Mathf.Sin(q*Mathf.PI)*.46f,-.35f+q*.38f),.027f,"Rubber",p);}
            // Preserve the limb pivots for the existing HTML walking animation.
            foreach(string limb in new[]{"LegLeft","LegRight","ArmLeft","ArmRight"})Combine(p.Find(limb).gameObject);
            var torso=Group("Torso",p);
            foreach(var f in g.GetComponentsInChildren<MeshFilter>().Where(f=>f.transform.parent==p).ToArray())f.transform.SetParent(torso.transform,true);
            Combine(torso);
            return g;
        }
        static GameObject Environment()
        {
            var g=Group("HarborEnvironment");var p=g.transform;
            Box("Floating concrete dock",V(16,.8f,25),V(5.5f,-.58f,10),"Concrete",p,.18f);
            Box("Deck substrate",V(11.12f,.19f,20.12f),V(5.5f,-.07f,10),"Dark",p,.03f);
            for(int side=0;side<2;side++)
            {
                float x=side==0?-1.02f:12.02f;
                Box("Drain channel",V(.3f,.05f,21.4f),V(side==0?-.2f:11.2f,.01f,10),"Dark",p,.015f);
                for(int i=0;i<84;i++)Box("Drain grille",V(.29f,.035f,.055f),V(side==0?-.2f:11.2f,.04f,-.35f+i*.25f),"Steel",p,.006f);
                Box("Parapet base",V(.47f,.44f,22.1f),V(x,.13f,10),"Concrete",p,.06f);
                Rod("Upper guard rail",V(x,1.13f,-1.1f),V(x,1.13f,21.1f),.054f,"Edge",p);
                Rod("Middle guard rail",V(x,.65f,-1.1f),V(x,.65f,21.1f),.038f,"Steel",p);
                for(int z=-1;z<=21;z+=2)
                {Box("Rail foot",V(.26f,.055f,.3f),V(x,.37f,z),"Steel",p,.015f);Rod("Rail post",V(x,.38f,z),V(x,1.17f,z),.062f,"Teal",p);}
                Rod("Service pipe",V(x-.14f,.09f,-1),V(x-.14f,.09f,21),.095f,"Rust",p);
                for(int z=0;z<=20;z+=4){var sleeve=Cylinder("Pipe collar",.115f,.1f,V(x-.14f,.09f,z),"Edge",p);sleeve.transform.localEulerAngles=V(90,0,0);}
            }
            foreach(float z in new[]{-1.06f,21.06f})
            {
                Box("End curb",V(12.9f,.5f,.48f),V(5.5f,.17f,z),"Concrete",p,.06f);
                Box("End coping",V(12.9f,.075f,.49f),V(5.5f,.46f,z),"Steel",p,.02f);
                for(int i=0;i<11;i++)Box("Edge marker",V(.43f,.022f,.12f),V(i+.5f,.502f,z),i%3==0?"Amber":"Ivory",p,.005f);
            }
            // Workshop machinery sits outside the playable lanes.
            foreach(var pos in new[]{V(-1.85f,0,2),V(12.85f,0,18)})
            {var pump=Pump();pump.transform.SetParent(p,false);pump.transform.localPosition=pos;pump.transform.localRotation=Quaternion.Euler(0,pos.x<0?90:-90,0);pump.transform.localScale=Vector3.one*.8f;}
            foreach(var pos in new[]{V(-1.75f,0,16),V(12.8f,0,4),V(10.5f,0,-1.8f),V(1,0,21.8f)}){var drum=Drum();drum.transform.SetParent(p,false);drum.transform.localPosition=pos;}
            foreach(var pos in new[]{V(-1.75f,0,-.8f),V(12.75f,0,-.8f),V(-1.75f,0,20.8f),V(12.75f,0,20.8f)})
            {var light=WorkLight();light.transform.SetParent(p,false);light.transform.localPosition=pos;light.transform.localRotation=Quaternion.Euler(0,pos.z<10?180:0,0);}
            // A distinct service gantry and water-side piping give the dock its identity.
            foreach(float x in new[]{-.95f,11.95f})
            {Box("Gantry upright",V(.24f,3.6f,.28f),V(x,1.7f,-1.35f),"Teal",p,.035f);Box("Mount plate",V(.5f,.06f,.55f),V(x,.02f,-1.35f),"Steel",p);}
            Box("Gantry beam",V(13.3f,.32f,.32f),V(5.5f,3.45f,-1.35f),"Teal",p,.035f);
            Box("Workshop sign",V(3.25f,.67f,.09f),V(5.5f,3.14f,-1.11f),"Ivory",p,.045f);
            // Three bars form an original abstract workshop mark, with no borrowed logos.
            for(int i=0;i<3;i++)Box("Doca mark",V(.14f,.34f-i*.07f,.018f),V(4.35f+i*.23f,3.14f,-1.052f),"Teal",p,.014f);
            for(int i=0;i<4;i++)Box("Sign dash",V(.23f,.06f,.018f),V(5.35f+i*.38f,3.14f,-1.052f),"Dark",p,.008f);
            Combine(g);return g;
        }
        static void SetupLighting()
        {
            RenderSettings.ambientMode=AmbientMode.Trilight;RenderSettings.ambientSkyColor=C("b0c5d4")*.65f;
            RenderSettings.ambientEquatorColor=C("6f8794")*.6f;RenderSettings.ambientGroundColor=C("4f5954")*.4f;
            RenderSettings.fog=false;
            var sun=new GameObject("Warm afternoon sun").AddComponent<Light>();sun.type=LightType.Directional;sun.color=C("ffebca");sun.intensity=1.3f;sun.transform.rotation=Quaternion.Euler(48,-35,0);sun.shadows=LightShadows.Soft;sun.shadowBias=.025f;sun.shadowNormalBias=.12f;
            var fill=new GameObject("Sky fill").AddComponent<Light>();fill.type=LightType.Directional;fill.color=C("8ebfdb");fill.intensity=.3f;fill.transform.rotation=Quaternion.Euler(35,145,0);
        }
        static void Capture(Camera camera,string path,int w,int h)
        {
            var rt=new RenderTexture(w,h,24,RenderTextureFormat.ARGB32){antiAliasing=4};rt.Create();camera.targetTexture=rt;camera.Render();
            var previous=RenderTexture.active;RenderTexture.active=rt;var t=new Texture2D(w,h,TextureFormat.RGB24,false);t.ReadPixels(new Rect(0,0,w,h),0,0);t.Apply();File.WriteAllBytes(path,t.EncodeToPNG());
            camera.targetTexture=null;RenderTexture.active=previous;rt.Release();UnityEngine.Object.DestroyImmediate(t);UnityEngine.Object.DestroyImmediate(rt);
        }
    }

    // Transparent intermediate export: the companion Python writer packs these into glTF 2.0.
    public static class NativeExport
    {
        [Serializable] public class Doc { public string generator="Unity 6000.3 — Tinta original procedural assets";public List<Node> nodes=new List<Node>();public List<Geo> meshes=new List<Geo>();public List<Surface> materials=new List<Surface>(); }
        [Serializable] public class Node {public string name;public int parent=-1;public Vector3 position,scale;public Quaternion rotation;public int mesh=-1,material=-1;}
        [Serializable] public class Geo {public Vector3[] positions,normals;public Vector2[] uv;public int[] indices;}
        [Serializable] public class Surface {public string name,texture;public Color color,emission;public float metallic,roughness;}
        public static void Write(GameObject root,string path)
        {
            var doc=new Doc();var meshes=new Dictionary<Mesh,int>();var materials=new Dictionary<Material,int>();var transforms=new Dictionary<Transform,int>();
            foreach(var t in root.GetComponentsInChildren<Transform>())
            {
                var n=new Node{name=t.name,parent=t==root.transform?-1:transforms[t.parent],position=t.localPosition,rotation=t.localRotation,scale=t.localScale};
                var f=t.GetComponent<MeshFilter>();if(f&&f.sharedMesh)
                {
                    var m=f.sharedMesh;if(!meshes.ContainsKey(m)){meshes[m]=doc.meshes.Count;doc.meshes.Add(new Geo{positions=m.vertices,normals=m.normals,uv=m.uv,indices=m.triangles});}n.mesh=meshes[m];
                    var mat=t.GetComponent<Renderer>().sharedMaterial;
                    if(!materials.ContainsKey(mat))
                    {materials[mat]=doc.materials.Count;doc.materials.Add(new Surface{name=mat.name,color=mat.color,metallic=mat.GetFloat("_Metallic"),roughness=1-mat.GetFloat("_Glossiness"),emission=mat.IsKeywordEnabled("_EMISSION")?mat.GetColor("_EmissionColor"):Color.black,texture=mat.mainTexture?Path.GetFullPath(AssetDatabase.GetAssetPath(mat.mainTexture)):null});}n.material=materials[mat];
                }
                transforms[t]=doc.nodes.Count;doc.nodes.Add(n);
            }
            File.WriteAllText(path,JsonUtility.ToJson(doc));
        }
    }
}
