﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Globalization;
using System.Xml;
using Oni.Xml;

namespace Oni.Dae.IO
{
    internal class DaeReader
    {
        public static string[] CommandLineArgs;

        #region Private data
        private static readonly string[] emptyStrings = new string[0];
        private static readonly char[] whiteSpaceChars = new char[] { ' ', '\t' };
        private static readonly Func<string, int> intConverter = XmlConvert.ToInt32;
        private static readonly Func<string, float> floatConverter = XmlConvert.ToSingle;
        private TextWriter error;
        private TextWriter info;
        private Scene mainScene;
        private Dictionary<string, Entity> entities;
        private XmlReader xml;
        private Axis upAxis = Axis.Y;
        private float unit = 1.0f;
        private List<Action> delayedBindActions;
        private Uri baseUrl;
        private string fileName;
        private List<Scene> scenes = new List<Scene>();
        private List<Light> lights = new List<Light>();
        private List<Animation> animations = new List<Animation>();
        private List<Geometry> geometries = new List<Geometry>();
        private List<Effect> effects = new List<Effect>();
        private List<Material> materials = new List<Material>();
        private List<Image> images = new List<Image>();
        private List<Camera> cameras = new List<Camera>();
        #endregion

        #region private class Animation

        private class Animation : Entity
        {
            private List<Animation> animations;
            private readonly List<Sampler> samplers = new List<Sampler>();

            public List<Animation> Animations
            {
                get
                {
                    if (animations == null)
                        animations = new List<Animation>();

                    return animations;
                }
            }

            public List<Sampler> Samplers => samplers;
        }

        #endregion

        public static Scene ReadFile(string filePath)
        {
            var reader = new DaeReader {
                baseUrl = new Uri("file://" + Path.GetDirectoryName(filePath).Replace('\\', '/').TrimEnd('/') + "/"),
                fileName = Path.GetFileName(filePath),
                delayedBindActions = new List<Action>(),
                error = Console.Error,
                info = Console.Out
            };

            var settings = new XmlReaderSettings {
                IgnoreWhitespace = true,
                IgnoreProcessingInstructions = true,
                IgnoreComments = true
            };

            using (reader.xml = XmlReader.Create(filePath, settings))
                reader.ReadRoot();

            return reader.mainScene;
        }

        private void ReadRoot()
        {
            while (xml.NodeType != XmlNodeType.Element)
                xml.Read();

            if (xml.LocalName != "COLLADA")
                throw new InvalidDataException(string.Format("Unknown root element {0} found", xml.LocalName));

            string version = xml.GetAttribute("version");

            if (version != "1.4.0" && version != "1.4.1")
                throw new NotSupportedException(string.Format("Unsupported Collada file version {0}", version));

            if (!xml.IsEmptyElement)
            {
                xml.ReadStartElement();

                ReadAsset();
                ReadContent();
                ReadExtra();
            }

            foreach (var action in delayedBindActions)
                action();

            if (mainScene == null && scenes.Count > 0)
                mainScene = scenes[0];

            BindNodes(mainScene);

            if (upAxis != Axis.Y)
                AxisConverter.Convert(mainScene, upAxis, Axis.Y);

            float scale = 1.0f;

            if (CommandLineArgs != null)
            {
                string scaleArg = Array.Find(CommandLineArgs, x => x.StartsWith("-dae-scale:", StringComparison.Ordinal));

                if (scaleArg != null)
                    scale = float.Parse(scaleArg.Substring(11), CultureInfo.InvariantCulture);
            }

            if (unit != 0.1f || scale != 1.0f)
                UnitConverter.Convert(mainScene, 10.0f * unit * scale);
        }

        private void ReadContent()
        {
            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "library_cameras":
                        ReadLibrary(cameras, "camera", ReadCamera);
                        break;

                    case "library_images":
                        ReadLibrary(images, "image", ReadImage);
                        break;

                    case "library_effects":
                        ReadLibrary(effects, "effect", ReadEffect);
                        break;

                    case "library_materials":
                        ReadLibrary(materials, "material", ReadMaterial);
                        break;

                    case "library_geometries":
                        ReadLibrary(geometries, "geometry", ReadGeometry);
                        break;

                    case "library_nodes":
                        ReadLibrary(scenes, "node", ReadNode);
                        break;

                    case "library_visual_scenes":
                        ReadLibrary(scenes, "visual_scene", ReadScene);
                        break;

                    case "library_animations":
                        ReadLibrary(animations, "animation", ReadAnimation);
                        break;

                    case "library_lights":
                        ReadLibrary(lights, "light", ReadLight);
                        break;

                    case "scene":
                        ReadScene();
                        break;

                    default:
                        xml.Skip();
                        break;
                }
            }
        }

        //
        // Libraries
        //

        private void ReadLibrary<T>(ICollection<T> library, string elementName, Action<T> entityReader)
            where T : Entity, new()
        {
            if (xml.SkipEmpty())
                return;

            xml.ReadStartElement();
            ReadAsset();

            while (xml.IsStartElement(elementName))
                ReadEntity<T>(library, entityReader);

            ReadExtra();
            xml.ReadEndElement();
        }

        private void ReadEntity<T>(ICollection<T> entityCollection, Action<T> entityReader)
            where T : Entity, new()
        {
            T entity = ReadEntity<T>(entityReader);
            entityCollection.Add(entity);
        }

        private T ReadEntity<T>(Action<T> entityReader)
            where T : Entity, new()
        {
            string id = xml.GetAttribute("id");

            var entity = new T {
                Name = xml.GetAttribute("name"),
                FileName = fileName
            };

            AddEntity(id, entity);

            if (string.IsNullOrEmpty(entity.Name))
                entity.Name = id;

            if (xml.IsEmptyElement)
            {
                xml.ReadStartElement();
                return entity;
            }

            xml.ReadStartElement();
            ReadAsset();

            entityReader(entity);

            ReadExtra();
            xml.ReadEndElement();

            return entity;
        }

        //
        // Cameras
        //

        private void ReadCamera(Camera camera)
        {
            ReadAsset();

            xml.ReadStartElement("optics");
            xml.ReadStartElement("technique_common");

            if (xml.IsStartElement("perspective"))
                ReadCameraParameters(camera, CameraType.Perspective);
            else if (xml.IsStartElement("orthographic"))
                ReadCameraParameters(camera, CameraType.Orthographic);
            else if (xml.IsStartElement())
                xml.Skip();

            xml.ReadEndElement();

            while (xml.IsStartElement())
                xml.Skip();

            xml.ReadEndElement();

            if (xml.IsStartElement("imager"))
                xml.Skip();

            ReadExtra();
        }

        private void ReadCameraParameters(Camera camera, CameraType type)
        {
            xml.ReadStartElement();

            camera.Type = type;

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "xfov":
                        camera.XFov = xml.ReadElementContentAsFloat();
                        break;
                    case "yfov":
                        camera.YFov = xml.ReadElementContentAsFloat();
                        break;
                    case "xmag":
                        camera.XMag = xml.ReadElementContentAsFloat();
                        break;
                    case "ymag":
                        camera.YMag = xml.ReadElementContentAsFloat();
                        break;
                    case "aspect_ratio":
                        camera.AspectRatio = xml.ReadElementContentAsFloat();
                        break;
                    case "znear":
                        camera.ZNear = xml.ReadElementContentAsFloat();
                        break;
                    case "zfar":
                        camera.ZFar = xml.ReadElementContentAsFloat();
                        break;

                    default:
                        xml.Skip();
                        break;
                }
            }

            xml.ReadEndElement();
        }

        //
        // Materials
        //

        private void ReadImage(Image image)
        {
            //image.Width = ReadNullableIntAttribute("width");
            //image.Height = ReadNullableIntAttribute("height");
            //image.Depth = ReadNullableIntAttribute("depth");

            ReadAsset();

            if (xml.IsStartElement("init_from"))
            {
                string filePath = xml.ReadElementContentAsString();

                if (!string.IsNullOrEmpty(filePath))
                {
                    var imageUri = new Uri(baseUrl, filePath);
                    image.FilePath = imageUri.LocalPath;
                }
            }
            else if (xml.IsStartElement("data"))
            {
                throw new NotSupportedException("Embedded image data is not supported");
            }
            else
            {
                throw new InvalidDataException();
            }

            ReadExtra();
        }

        private void ReadEffect(Effect effect)
        {
            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "image":
                        ReadEntity(images, ReadImage);
                        break;
                    case "newparam":
                        ReadEffectParameterDecl(effect);
                        break;
                    case "profile_COMMON":
                        ReadEffectProfileCommon(effect);
                        break;
                    default:
                        xml.Skip();
                        break;
                }
            }

            ReadExtra();
        }

        private void ReadEffectProfileCommon(Effect effect)
        {
            xml.ReadStartElement();
            ReadAsset();

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "image":
                        ReadEntity(images, ReadImage);
                        break;
                    case "newparam":
                        ReadEffectParameterDecl(effect);
                        break;
                    case "technique":
                        ReadEffectTechniqueCommon(effect);
                        break;
                    default:
                        xml.Skip();
                        break;
                }
            }

            ReadExtra();
            xml.ReadEndElement();
        }

        private void ReadEffectTechniqueCommon(Effect effect)
        {
            xml.ReadStartElement();
            ReadAsset();

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "image":
                        ReadEntity(images, ReadImage);
                        break;

                    case "constant":
                    case "lambert":
                    case "phong":
                    case "blinn":
                        xml.ReadStartElement();
                        ReadEffectTechniqueParameters(effect);
                        xml.ReadEndElement();
                        break;

                    default:
                        xml.Skip();
                        break;
                }
            }

            ReadExtra();
            xml.ReadEndElement();
        }

        private void ReadEffectParameterDecl(Effect effect)
        {
            var parameter = new EffectParameter();
            parameter.Sid = xml.GetAttribute("sid");

            xml.ReadStartElement();

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "semantic":
                        parameter.Semantic = xml.ReadElementContentAsString();
                        break;
                    case "float":
                        parameter.Value = xml.ReadElementContentAsFloat();
                        break;
                    case "float2":
                        parameter.Value = xml.ReadElementContentAsVector2();
                        break;
                    case "float3":
                        parameter.Value = xml.ReadElementContentAsVector3();
                        break;
                    case "surface":
                        parameter.Value = ReadEffectSurface(effect);
                        break;
                    case "sampler2D":
                        parameter.Value = ReadEffectSampler2D(effect);
                        break;
                    default:
                        xml.Skip();
                        break;
                }
            }

            xml.ReadEndElement();

            effect.Parameters.Add(parameter);
        }

        private EffectSurface ReadEffectSurface(Effect effect)
        {
            var surface = new EffectSurface();

            xml.ReadStartElement();

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "init_from":
                        BindId<Image>(xml.ReadElementContentAsString(), image => {
                            surface.InitFrom = image;
                        });
                        break;

                    default:
                        xml.Skip();
                        break;
                }
            }

            xml.ReadEndElement();
            return surface;
        }

        private EffectSampler ReadEffectSampler2D(Effect effect)
        {
            var sampler = new EffectSampler();

            xml.ReadStartElement();

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "source":
                        string surfaceSid = xml.ReadElementContentAsString();
                        foreach (var parameter in effect.Parameters)
                        {
                            if (parameter.Sid == surfaceSid)
                                sampler.Surface = parameter.Value as EffectSurface;
                        }
                        break;
                    case "wrap_s":
                        sampler.WrapS = (EffectSamplerWrap)Enum.Parse(typeof(EffectSamplerWrap), xml.ReadElementContentAsString(), true);
                        break;
                    case "wrap_t":
                        sampler.WrapT = (EffectSamplerWrap)Enum.Parse(typeof(EffectSamplerWrap), xml.ReadElementContentAsString(), true);
                        break;
                    default:
                        xml.Skip();
                        break;
                }
            }

            xml.ReadEndElement();
            return sampler;
        }

        private void ReadEffectTechniqueParameters(Effect effect)
        {
            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "emission":
                        ReadColorEffectParameter(effect, effect.Emission, EffectTextureChannel.Emission);
                        break;
                    case "ambient":
                        ReadColorEffectParameter(effect, effect.Ambient, EffectTextureChannel.Ambient);
                        break;
                    case "diffuse":
                        ReadColorEffectParameter(effect, effect.Diffuse, EffectTextureChannel.Diffuse);
                        break;
                    case "specular":
                        ReadColorEffectParameter(effect, effect.Specular, EffectTextureChannel.Specular);
                        break;
                    case "shininess":
                        ReadFloatEffectParameter(effect.Shininess);
                        break;
                    case "reflective":
                        ReadColorEffectParameter(effect, effect.Reflective, EffectTextureChannel.Reflective);
                        break;
                    case "reflectivity":
                        ReadFloatEffectParameter(effect.Reflectivity);
                        break;
                    case "transparent":
                        ReadColorEffectParameter(effect, effect.Transparent, EffectTextureChannel.Transparent);
                        break;
                    case "transparency":
                        ReadFloatEffectParameter(effect.Transparency);
                        break;
                    case "index_of_refraction":
                        ReadFloatEffectParameter(effect.IndexOfRefraction);
                        break;
                    default:
                        xml.Skip();
                        break;
                }
            }
        }

        private void ReadFloatEffectParameter(EffectParameter parameter)
        {
            xml.ReadStartElement();

            if (xml.IsStartElement("float"))
            {
                parameter.Sid = xml.GetAttribute("sid");
                parameter.Value = xml.ReadElementContentAsFloat();
            }
            else if (xml.IsStartElement("param"))
            {
                parameter.Reference = xml.GetAttribute("ref");
                xml.Skip();
            }

            xml.ReadEndElement();
        }

        private void ReadColorEffectParameter(Effect effect, EffectParameter parameter, EffectTextureChannel channel)
        {
            xml.ReadStartElement();

            if (xml.IsStartElement("color"))
            {
                parameter.Sid = xml.GetAttribute("sid");
                parameter.Value = xml.ReadElementContentAsVector4();
            }
            else if (xml.IsStartElement("param"))
            {
                parameter.Sid = null;
                parameter.Reference = xml.GetAttribute("ref");
            }
            else if (xml.IsStartElement("texture"))
            {
                parameter.Sid = null;

                string texCoordSymbol = xml.GetAttribute("texcoord");
                string samplerId = xml.GetAttribute("texture");
                xml.Skip();

                //
                // HACK: Maya produces duplicate texture elements, skip them...
                //

                while (xml.IsStartElement("texture"))
                    xml.Skip();

                EffectSampler sampler = null;

                foreach (var parameterDecl in effect.Parameters)
                {
                    if (parameterDecl.Sid == samplerId)
                    {
                        sampler = parameterDecl.Value as EffectSampler;
                        break;
                    }
                }

                if (sampler == null)
                {
                    info.WriteLine("COLLADA: cannot find sampler {0} in effect {1}, trying to use image directly", samplerId, effect.Name);

                    var surface = new EffectSurface();
                    sampler = new EffectSampler(surface);

                    BindId<Image>(samplerId, image => {
                        surface.InitFrom = image;
                    });
                }

                var texture = new EffectTexture {
                    Channel = channel,
                    TexCoordSemantic = texCoordSymbol,
                    Sampler = sampler
                };

                parameter.Value = texture;
            }

            xml.ReadEndElement();
        }

        private void ReadMaterial(Material material)
        {
            if (!xml.IsStartElement("instance_effect"))
                return;

            BindUrlAttribute<Effect>("url", effect => {
                material.Effect = effect;
            });

            xml.Skip();
        }

        //
        // Geometry
        //

        private void ReadGeometry(Geometry geometry)
        {
            if (xml.IsStartElement("mesh"))
            {
                ReadMesh(geometry);
            }
            else
            {
                throw new NotSupportedException(string.Format("Geometry content of type {0} is not supported", xml.LocalName));
            }
        }

        //
        // Mesh
        //

        private void ReadMesh(Geometry geometry)
        {
            xml.ReadStartElement();

            while (xml.IsStartElement("source"))
                ReadGeometrySource();

            if (xml.IsStartElement("vertices"))
                ReadMeshVertices(geometry);

            while (xml.IsStartElement())
            {
                var primitives = ReadMeshPrimitives(geometry);

                if (primitives == null)
                    break;

                geometry.Primitives.Add(primitives);
            }

            ReadExtra();
            xml.ReadEndElement();
        }

        //
        // Mesh Data Source
        //

        private Source ReadGeometrySource()
        {
            string id = xml.GetAttribute("id");
            string name = xml.GetAttribute("name");

            xml.ReadStartElement();
            ReadAsset();

            var data = ReadFloatArray();
            var source = new Source(data, 1) {
                Name = name
            };

            if (xml.IsStartElement("technique_common"))
            {
                xml.ReadStartElement();

                if (xml.IsStartElement("accessor"))
                {
                    source.Stride = ReadIntAttribute("stride", 1);

                    xml.ReadStartElement();

                    while (xml.IsStartElement("param"))
                        ReadParam();

                    xml.ReadEndElement();
                }

                xml.ReadEndElement();
            }

            xml.SkipSequence("technique");
            xml.ReadEndElement();

            AddEntity(id, source);

            return source;
        }

        private string ReadParam()
        {
            string name = xml.GetAttribute("name");
            //string sid = xml.GetAttribute("sid");
            //string type = xml.GetAttribute("type");
            //string semantic = xml.GetAttribute("semantic");

            xml.Skip();

            return name;
        }

        //
        // Mesh Vertices
        //

        private void ReadMeshVertices(Geometry mesh)
        {
            string id = xml.GetAttribute("id");

            for (xml.ReadStartElement(); xml.IsStartElement("input"); xml.Skip())
            {
                var semantic = ReadSemanticAttribute();

                if (semantic != Semantic.None && semantic != Semantic.Vertex)
                {
                    var input = new Input();
                    input.Semantic = semantic;
                    BindUrlAttribute<Source>("source", s => {
                        input.Source = s;
                    });
                    mesh.Vertices.Add(input);
                }
            }

            ReadExtra();
            xml.ReadEndElement();
        }

        //
        // Mesh Polygons
        //

        private MeshPrimitives ReadMeshPrimitives(Geometry mesh)
        {
            MeshPrimitives primitives;

            int primitiveCount = ReadIntAttribute("count", 0);
            int fixedVertexCount = 0;
            bool isPolygons = false;

            switch (xml.LocalName)
            {
                case "lines":
                    primitives = new MeshPrimitives(MeshPrimitiveType.Lines);
                    fixedVertexCount = 2;
                    break;
                case "triangles":
                    primitives = new MeshPrimitives(MeshPrimitiveType.Polygons);
                    fixedVertexCount = 3;
                    break;

                case "linestrips":
                    primitives = new MeshPrimitives(MeshPrimitiveType.LineStrips);
                    isPolygons = true;
                    break;
                case "trifans":
                    primitives = new MeshPrimitives(MeshPrimitiveType.TriangleFans);
                    isPolygons = true;
                    break;
                case "tristrips":
                    primitives = new MeshPrimitives(MeshPrimitiveType.TriangleStrips);
                    isPolygons = true;
                    break;
                case "polygons":
                    primitives = new MeshPrimitives(MeshPrimitiveType.Polygons);
                    isPolygons = true;
                    break;

                case "polylist":
                    primitives = new MeshPrimitives(MeshPrimitiveType.Polygons);
                    break;

                default:
                    return null;
            }

            primitives.MaterialSymbol = xml.GetAttribute("material");

            bool vertexFound = false;

            //
            // Read the inputs
            //

            for (xml.ReadStartElement(); xml.IsStartElement("input"); xml.Skip())
            {
                var semantic = ReadSemanticAttribute();

                if (semantic == Semantic.None)
                    continue;

                int offset = ReadIntAttribute("offset");
                string sourceId = xml.GetAttribute("source");
                int set = ReadIntAttribute("set", -1);

                if (semantic == Semantic.Vertex)
                {
                    if (vertexFound)
                    {
                        error.WriteLine("Duplicate vertex input found");
                        continue;
                    }

                    vertexFound = true;

                    foreach (var vertexInput in mesh.Vertices)
                    {
                        primitives.Inputs.Add(new IndexedInput {
                            Source = vertexInput.Source,
                            Offset = offset,
                            Set = set,
                            Semantic = vertexInput.Semantic
                        });
                    }
                }
                else
                {
                    var input = new IndexedInput {
                        Offset = offset,
                        Semantic = semantic,
                        Set = set
                    };

                    BindUrl<Source>(sourceId, s => {

                        //
                        // Ignore inputs with no source data
                        //

                        if (s.Count > 0)
                        {
                            input.Source = s;
                            primitives.Inputs.Add(input);
                        }
                    });
                }
            }

            if (!vertexFound)
                throw new InvalidDataException("no vertex input");

            //
            // Read vertex counts (if availabled and needed)
            //

            if (primitiveCount > 0)
                primitives.VertexCounts.Capacity = primitiveCount;

            int numIndices = 0;

            while (xml.IsStartElement("vcount"))
            {
                if (fixedVertexCount != 0 || isPolygons)
                {
                    xml.Skip();
                    continue;
                }

                foreach (var token in xml.ReadElementContentAsList())
                {
                    int count = XmlConvert.ToInt32(token);
                    numIndices += count;
                    primitives.VertexCounts.Add(count);
                }
            }

            if (fixedVertexCount != 0)
            {
                for (int i = 0; i < primitiveCount; i++)
                    primitives.VertexCounts.Add(fixedVertexCount);

                numIndices = fixedVertexCount * primitiveCount;
            }
            else if (!isPolygons)
            {
                if (primitives.VertexCounts.Count == 0)
                    throw new InvalidDataException("no vcount");
            }

            //
            // Read input indices
            // 1. Collect all inputs in an array indexed by input offset
            //

            var maxOffset = primitives.Inputs.Max(x => x.Offset);
            var inputIndices = new List<int>[maxOffset + 1];

            foreach (var input in primitives.Inputs)
            {
                List<int> indices = inputIndices[input.Offset];

                if (indices == null)
                {
                    indices = new List<int>(numIndices);
                    inputIndices[input.Offset] = indices;
                }
            }

            //
            // 2. Read polygon input indices
            //

            if (!isPolygons)
            {
                while (xml.IsStartElement("p"))
                    ReadInterleavedInputIndices(inputIndices);
            }
            else
            {
                while (xml.IsStartElement())
                {
                    if (xml.IsStartElement("p"))
                    {
                        primitives.VertexCounts.Add(ReadInterleavedInputIndices(inputIndices));
                    }
                    else if (xml.IsStartElement("ph"))
                    {
                        xml.ReadStartElement();

                        while (xml.IsStartElement())
                        {
                            if (xml.LocalName == "p")
                                primitives.VertexCounts.Add(ReadInterleavedInputIndices(inputIndices));
                            else
                                xml.Skip();
                        }

                        xml.ReadEndElement();
                    }
                    else
                    {
                        break;
                    }
                }
            }

            foreach (var input in primitives.Inputs)
                input.Indices.AddRange(inputIndices[input.Offset]);

            ReadExtra();
            xml.ReadEndElement();

            return primitives;
        }

        private int ReadInterleavedInputIndices(List<int>[] inputs)
        {
            int count = 0;
            int offset = 0;

            foreach (string token in xml.ReadElementContentAsList())
            {
                var input = inputs[offset++];

                if (input != null)
                    input.Add(XmlConvert.ToInt32(token));

                if (offset >= inputs.Length)
                {
                    offset = 0;
                    count++;
                }
            }

            return count;
        }

        //
        // Scene
        //

        private void ReadScene(Scene scene)
        {
            while (xml.IsStartElement("node"))
                ReadEntity(scene.Nodes, ReadNode);
        }

        private void ReadNode(Node node)
        {
            ReadTransforms(node.Transforms);

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "node":
                        ReadEntity(node.Nodes, ReadNode);
                        break;

                    case "instance_geometry":
                        node.Instances.Add(ReadGeometryInstance());
                        break;

                    case "instance_light":
                        node.Instances.Add(ReadLightInstance());
                        break;

                    case "instance_camera":
                        node.Instances.Add(ReadCameraInstance());
                        break;

                    case "instance_node":
                        node.Instances.Add(ReadNodeInstance());
                        break;

                    default:
                        xml.Skip();
                        break;
                }
            }
        }

        private void ReadSimpleInstance<T>(Instance<T> instance)
            where T : Entity
        {
            instance.Sid = xml.GetAttribute("sid");
            instance.Name = xml.GetAttribute("name");
            BindUrlAttribute<T>("url", camera => {
                instance.Target = camera;
            });

            xml.Skip();
        }

        private NodeInstance ReadNodeInstance()
        {
            var instance = new NodeInstance();
            ReadSimpleInstance(instance);
            return instance;
        }

        private CameraInstance ReadCameraInstance()
        {
            var instance = new CameraInstance();
            ReadSimpleInstance(instance);
            return instance;
        }

        private LightInstance ReadLightInstance()
        {
            var instance = new LightInstance();
            ReadSimpleInstance(instance);
            return instance;
        }

        //
        // Node Transforms
        //

        private void ReadTransforms(ICollection<Transform> transforms)
        {
            while (xml.IsStartElement())
            {
                Transform transform = null;

                switch (xml.LocalName)
                {
                    case "matrix":
                        transform = new TransformMatrix();
                        break;
                    case "rotate":
                        transform = new TransformRotate();
                        break;
                    case "scale":
                        transform = new TransformScale();
                        break;
                    case "translate":
                        transform = new TransformTranslate();
                        break;

                    case "skew":
                    case "lookat":
                        xml.Skip();
                        break;

                    default:
                        return;
                }

                if (transform != null)
                {
                    transform.Sid = xml.GetAttribute("sid");
                    xml.ReadElementContentAsArray(floatConverter, transform.Values);
                    transforms.Add(transform);
                }
            }
        }

        //
        // Instances
        //

        private void ReadInstances(ICollection<Instance> instances)
        {
            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "instance_geometry":
                        instances.Add(ReadGeometryInstance());
                        break;

                    case "instance_camera":
                    case "instance_controller":
                    case "instance_light":
                    case "instance_node":
                        xml.Skip();
                        break;

                    default:
                        return;
                }
            }
        }

        private GeometryInstance ReadGeometryInstance()
        {
            var instance = new GeometryInstance {
                Name = xml.GetAttribute("name"),
                Sid = xml.GetAttribute("sid"),
            };

            string url = xml.GetAttribute("url");
            BindUrl<Geometry>(url, geometry => {
                instance.Target = geometry;
            });

            if (!xml.SkipEmpty())
            {
                xml.ReadStartElement();

                if (xml.IsStartElement("bind_material"))
                    ReadBindMaterial(instance, url);

                ReadExtra();

                xml.ReadEndElement();
            }

            return instance;
        }

        private void ReadBindMaterial(GeometryInstance geometryInstance, string geometryUrl)
        {
            xml.ReadStartElement();

            while (xml.IsStartElement())
            {
                if (xml.LocalName != "technique_common")
                {
                    xml.Skip();
                    continue;
                }

                xml.ReadStartElement();

                while (xml.IsStartElement())
                {
                    if (xml.LocalName == "instance_material")
                        ReadMaterialInstance(geometryInstance, geometryUrl);
                    else
                        xml.Skip();
                }

                xml.ReadEndElement();
            }

            xml.ReadEndElement();
        }

        private void ReadMaterialInstance(GeometryInstance geometryInstance, string geometryUrl)
        {
            var instance = new MaterialInstance();
            instance.Symbol = xml.GetAttribute("symbol");
            BindUrlAttribute<Material>("target", material => {
                instance.Target = material;
            });
            geometryInstance.Materials.Add(instance);

            if (xml.SkipEmpty())
                return;

            for (xml.ReadStartElement(); xml.IsStartElement(); xml.Skip())
            {
                if (xml.LocalName == "bind")
                {
                    var binding = new MaterialBinding();
                    binding.Semantic = xml.GetAttribute("semantic");
                    string target = xml.GetAttribute("target");

                    BindId<Source>(target, s => {
                        BindUrl<Geometry>(geometryUrl, g => {
                            var primitives = g.Primitives.Find(p => p.MaterialSymbol == instance.Symbol);

                            if (primitives == null)
                                return;

                            var input = primitives.Inputs.Find(i => i.Source == s);

                            if (input == null)
                                return;

                            binding.VertexInput = input;
                            instance.Bindings.Add(binding);
                        });
                    });
                }
                else if (xml.LocalName == "bind_vertex_input")
                {
                    var binding = new MaterialBinding();
                    binding.Semantic = xml.GetAttribute("semantic");
                    var inputSemantic = ReadSemanticAttribute("input_semantic");
                    int inputSet = ReadIntAttribute("input_set", 0);

                    BindUrl<Geometry>(geometryUrl, g => {
                        var primitives = g.Primitives.Find(p => p.MaterialSymbol == instance.Symbol);

                        if (primitives == null)
                            return;

                        var input = primitives.Inputs.Find(i => i.Semantic == inputSemantic && i.Set == inputSet);

                        if (input == null)
                            return;

                        binding.VertexInput = input;
                        instance.Bindings.Add(binding);
                    });
                }
            }

            xml.ReadEndElement();
        }

        //
        // Animations
        //

        private void ReadAnimation(Animation animation)
        {
            while (xml.IsStartElement("animation"))
                ReadEntity(animation.Animations, ReadAnimation);

            while (xml.IsStartElement("source"))
                ReadAnimationSource();

            while (xml.IsStartElement("sampler"))
                animation.Samplers.Add(ReadAnimationSampler());

            while (xml.IsStartElement("channel"))
            {
                BindAnimationSampler(xml.GetAttribute("source"), xml.GetAttribute("target"));
                xml.Skip();
            }
        }

        private Source ReadAnimationSource()
        {
            string id = xml.GetAttribute("id");
            string name = xml.GetAttribute("name");

            if (string.IsNullOrEmpty(name))
                name = id;

            xml.ReadStartElement();

            ReadAsset();

            Source source;

            if (xml.IsStartElement("float_array"))
                source = new Source(ReadFloatArray(), 1);
            else if (xml.IsStartElement("Name_array"))
                source = new Source(ReadNameArray(), 1);
            else
                throw new NotSupportedException(string.Format("Animation sources of type {0} are not supported", xml.LocalName));

            source.Name = name;

            if (xml.IsStartElement("technique_common"))
            {
                xml.ReadStartElement();

                if (xml.IsStartElement("accessor"))
                {
                    source.Stride = ReadIntAttribute("stride", 1);

                    xml.ReadStartElement();

                    while (xml.IsStartElement("param"))
                        ReadParam();

                    xml.ReadEndElement();
                }

                xml.ReadEndElement();
            }

            xml.SkipSequence("technique");
            xml.ReadEndElement();

            AddEntity(id, source);

            return source;
        }

        private Input ReadAnimationInput()
        {
            var input = new Input();
            input.Semantic = ReadSemanticAttribute();
            BindUrlAttribute<Source>("source", source => {
                input.Source = source;
            });

            xml.Skip();

            return input;
        }

        private Sampler ReadAnimationSampler()
        {
            string id = xml.GetAttribute("id");

            xml.ReadStartElement();

            var sampler = new Sampler();

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "input":
                        sampler.Inputs.Add(ReadAnimationInput());
                        break;

                    default:
                        xml.Skip();
                        break;
                }
            }

            xml.ReadEndElement();

            AddEntity(id, sampler);

            return sampler;
        }

        #region private class TargetPath

        private class TargetPath
        {
            private string nodeId;
            private string[] path;
            private string value;

            private TargetPath()
            {
            }

            public static TargetPath Parse(string text)
            {
                TargetPath path = new TargetPath();
                List<string> sids = new List<string>();

                int index = text.IndexOf('/');

                if (index == -1)
                    index = text.Length;

                path.nodeId = text.Substring(0, index);

                for (int start = index + 1; start < text.Length; start = index + 1)
                {
                    index = text.IndexOf('/', start);

                    if (index == -1)
                    {
                        index = text.IndexOf('.', start);

                        if (index == -1)
                        {
                            sids.Add(text.Substring(start));
                        }
                        else
                        {
                            sids.Add(text.Substring(start, index - start));
                            path.value = text.Substring(index + 1);
                        }

                        break;
                    }

                    sids.Add(text.Substring(start, index - start));
                }

                if (sids.Count > 0)
                    path.path = sids.ToArray();

                return path;
            }

            public string NodeId => nodeId;
            public string[] Path => path;
            public string Value => value;
        }

        #endregion

        //
        // Lights
        //

        private void ReadLight(Light light)
        {
            if (!xml.IsStartElement("technique_common"))
            {
                xml.Skip();
                return;
            }

            xml.ReadStartElement();

            if (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "ambient":
                        light.Type = LightType.Ambient;
                        break;

                    case "directional":
                        light.Type = LightType.Directional;
                        break;

                    case "point":
                        light.Type = LightType.Point;
                        break;

                    case "spot":
                        light.Type = LightType.Spot;
                        break;
                }

                xml.ReadStartElement();

                light.Color = xml.ReadElementContentAsVector3("color");

                if (light.Type == LightType.Point || light.Type == LightType.Spot)
                {
                    if (xml.LocalName == "constant_attenuation")
                        light.ConstantAttenuation = xml.ReadElementContentAsFloat();

                    if (xml.LocalName == "linear_attenuation")
                        light.LinearAttenuation = xml.ReadElementContentAsFloat();

                    if (light.Type == LightType.Point)
                    {
                        light.QuadraticAttenuation = xml.ReadElementContentAsFloat("quadratic_attenuation", string.Empty);

                        if (xml.LocalName == "zfar")
                            light.ZFar = xml.ReadElementContentAsFloat();
                    }
                    else if (light.Type == LightType.Spot)
                    {
                        if (xml.LocalName == "quadratic_attenuation")
                            light.QuadraticAttenuation = xml.ReadElementContentAsFloat();

                        if (xml.LocalName == "falloff_angle")
                            light.FalloffAngle = xml.ReadElementContentAsFloat();

                        if (xml.LocalName == "falloff_exponent")
                            light.FalloffExponent = xml.ReadElementContentAsFloat();
                    }
                }

                xml.ReadEndElement();
            }

            xml.ReadEndElement();
        }

        //
        // Scene
        //

        private void ReadScene()
        {
            if (!xml.IsStartElement("scene"))
                return;

            xml.ReadStartElement();
            xml.SkipSequence("instance_physics_scene");

            if (xml.IsStartElement("instance_visual_scene"))
            {
                BindUrlAttribute<Scene>("url", scene => {
                    mainScene = scene;
                });
                xml.Skip();
            }

            ReadExtra();
            xml.ReadEndElement();
        }

        //
        // Others
        //

        private void ReadAsset()
        {
            if (!xml.IsStartElement("asset"))
                return;

            xml.ReadStartElement();

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "up_axis":
                        upAxis = ReadUpAxis();
                        break;
                    case "contributor":
                        ReadAssetContributor();
                        break;
                    case "unit":
                        unit = XmlConvert.ToSingle(xml.GetAttribute("meter"));
                        xml.Skip();
                        break;
                    default:
                        xml.Skip();
                        break;
                }
            }

            xml.ReadEndElement();
        }

        private void ReadAssetContributor()
        {
            xml.ReadStartElement();

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    default:
                        xml.Skip();
                        break;
                }
            }

            xml.ReadEndElement();
        }

        private void ReadExtra()
        {
            while (xml.IsStartElement("extra"))
                xml.Skip();
        }

        //
        // Arrays
        //

        private float[] ReadFloatArray()
        {
            if (!xml.IsStartElement("float_array"))
                return null;

            var id = xml.GetAttribute("id");
            var count = ReadIntAttribute("count");
            var data = new float[count];
            var index = 0;

            foreach (var token in xml.ReadElementContentAsList())
            {
                if (index < data.Length)
                    data[index++] = XmlConvert.ToSingle(token);
            }

            return data;
        }

        private string[] ReadNameArray()
        {
            if (!xml.IsStartElement("Name_array"))
                return null;

            var id = xml.GetAttribute("id");
            var count = ReadIntAttribute("count");
            var data = new string[count];
            var index = 0;

            foreach (var token in xml.ReadElementContentAsList())
            {
                if (index < data.Length)
                    data[index++] = token;
            }

            return data;
        }

        //
        // Attributes
        //

        private int ReadIntAttribute(string name)
        {
            string text = xml.GetAttribute(name);

            if (string.IsNullOrEmpty(text))
                throw new InvalidDataException(name + " attribute not found");

            return XmlConvert.ToInt32(text);
        }

        private int ReadIntAttribute(string name, int defaultValue)
        {
            string text = xml.GetAttribute(name);

            if (string.IsNullOrEmpty(text))
                return defaultValue;

            return XmlConvert.ToInt32(text);
        }

        private int? ReadNullableIntAttribute(string name)
        {
            string text = xml.GetAttribute(name);

            if (string.IsNullOrEmpty(text))
                return null;

            return XmlConvert.ToInt32(text);
        }

        private Semantic ReadSemanticAttribute()
        {
            return ReadSemanticAttribute("semantic");
        }

        private Semantic ReadSemanticAttribute(string name)
        {
            string text = xml.GetAttribute(name);

            switch (text)
            {
                case "POSITION":
                    return Semantic.Position;
                case "TEXCOORD":
                    return Semantic.TexCoord;
                case "NORMAL":
                    return Semantic.Normal;
                case "COLOR":
                    return Semantic.Color;
                case "VERTEX":
                    return Semantic.Vertex;

                case "INPUT":
                    return Semantic.Input;
                case "IN_TANGENT":
                    return Semantic.InTangent;
                case "OUT_TANGENT":
                    return Semantic.OutTangent;
                case "INTERPOLATION":
                    return Semantic.Interpolation;
                case "OUTPUT":
                    return Semantic.Output;

                default:
                    return Semantic.None;
            }
        }

        private string[] ReadStringListAttribute(string name)
        {
            string text = xml.GetAttribute(name);

            if (string.IsNullOrEmpty(text))
                return emptyStrings;

            return text.Split(whiteSpaceChars, StringSplitOptions.RemoveEmptyEntries);
        }

        private Axis ReadUpAxis()
        {
            switch (xml.ReadElementContentAsString())
            {
                case "X_UP":
                    return Axis.X;
                case "Z_UP":
                    return Axis.Z;
                default:
                    return Axis.Y;
            }
        }

        private void AddEntity(string id, Entity entity)
        {
            if (string.IsNullOrEmpty(id))
                return;

            if (entities == null)
                entities = new Dictionary<string, Entity>();

            if (entities.ContainsKey(id))
                error.WriteLine(string.Format("COLLADA error: duplicate id {0}", id));
            else
                entities.Add(id, entity);

            entity.Id = id;
        }

        private T GetEntity<T>(string id) where T : Entity
        {
            Entity result;

            if (entities == null
                || string.IsNullOrEmpty(id)
                || !entities.TryGetValue(id, out result))
                return null;

            return result as T;
        }

        private void BindUrlAttribute<T>(string name, Action<T> action)
            where T : Entity
        {
            BindUrl(xml.GetAttribute(name), action);
        }

        private void BindUrl<T>(string url, Action<T> action)
            where T : Entity
        {
            if (string.IsNullOrEmpty(url))
                return;

            if (url[0] != '#')
                throw new NotSupportedException(string.Format("External reference '{0}' is not supported", url));

            BindId<T>(url.Substring(1), action);
        }

        private void BindId<T>(string id, Action<T> action)
            where T : Entity
        {
            if (string.IsNullOrEmpty(id))
                return;

            var entity = GetEntity<T>(id);

            if (entity != null)
            {
                action(entity);
                return;
            }

            delayedBindActions.Add(delegate {
                var entity2 = GetEntity<T>(id);

                if (entity2 != null)
                    action(entity2);
            });
        }

        private void BindAnimationSampler(string sourceId, string targetPath)
        {
            var path = TargetPath.Parse(targetPath);

            BindUrlAttribute<Sampler>("source", sampler => {
                var output = sampler.Inputs.Find(i => i.Semantic == Semantic.Output);

                if (output == null || output.Source == null)
                    return;

                int stride = output.Source.Stride;

                if (stride != 1)
                {
                    for (int offset = 0; offset < stride; offset++)
                    {
                        var newSampler = sampler.Split(offset);

                        BindId<Node>(path.NodeId, node => {
                            Transform transform = FindTransform(node, path.Path[0]);

                            if (transform != null)
                                transform.BindAnimation(string.Format(CultureInfo.InvariantCulture, "({0})", offset), newSampler);
                        });
                    }
                }
                else
                {
                    BindId<Node>(path.NodeId, node => {
                        var transform = FindTransform(node, path.Path[0]);

                        if (transform != null)
                            transform.BindAnimation(path.Value, sampler);
                    });
                }
            });
        }

        private Transform FindTransform(Node node, string sid)
        {
            return node.Transforms.Find(t => t.Sid == sid);
        }

        private void BindNodes(Node node)
        {
            foreach (var instance in node.Instances.OfType<NodeInstance>().ToList())
            {
                node.Instances.Remove(instance);

                if (instance.Target != node)
                    node.Nodes.Add(instance.Target);
            }

            foreach (var child in node.Nodes)
                BindNodes(child);
        }
    }
}
