﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;

namespace Oni.Motoko
{
    using Imaging;
    using Metadata;

    internal class TextureImporter3
    {
        private readonly string outputDirPath;
        private readonly Dictionary<string, TextureImporterOptions> textures = new Dictionary<string, TextureImporterOptions>(StringComparer.Ordinal);
        private TextureFormat defaultFormat = TextureFormat.BGR;
        private TextureFormat defaultSquareFormat = TextureFormat.BGR;
        private TextureFormat defaultAlphaFormat = TextureFormat.RGBA;
        private int maxSize = 512;

        public TextureImporter3(string outputDirPath)
        {
            this.outputDirPath = outputDirPath;
        }

        public TextureFormat DefaultFormat
        {
            get
            {
                return defaultFormat;
            }
            set
            {
                defaultFormat = value;
                defaultSquareFormat = value;
            }
        }

        public TextureFormat DefaultAlphaFormat
        {
            get { return defaultAlphaFormat; }
            set { defaultAlphaFormat = value; }
        }

        public int MaxSize
        {
            get { return maxSize; }
            set { maxSize = value; }
        }

        public string AddMaterial(Dae.Material material)
        {
            if (material == null || material.Effect == null)
                return null;

            var texture = material.Effect.Textures.FirstOrDefault(t => t.Channel == Dae.EffectTextureChannel.Diffuse);

            if (texture == null)
                return null;

            var sampler = texture.Sampler;

            if (sampler == null || sampler.Surface == null || sampler.Surface.InitFrom == null)
                return null;

            var filePath = Path.GetFullPath(sampler.Surface.InitFrom.FilePath);

            if (!File.Exists(filePath))
                return null;

            var options = GetOptions(Path.GetFileNameWithoutExtension(filePath), filePath);

            return options.Name;
        }

        public TextureImporterOptions AddMaterial(Akira.Material material)
        {
            return GetOptions(material.Name, material.ImageFilePath);
        }

        private TextureImporterOptions GetOptions(string name, string filePath)
        {
            TextureImporterOptions options;

            if (!textures.TryGetValue(name, out options))
            {
                options = new TextureImporterOptions
                {
                    Name = name,
                    Images = new[] { filePath }
                };

                textures.Add(name, options);
            }

            return options;
        }

        public TextureImporterOptions GetOptions(string name, bool create)
        {
            TextureImporterOptions options;

            if (!textures.TryGetValue(name, out options) && create)
            {
                options = new TextureImporterOptions
                {
                    Name = name
                };

                textures.Add(name, options);
            }

            return options;
        }

        public void ReadOptions(XmlReader xml, string basePath)
        {
            var options = GetOptions(xml.GetAttribute("Name"), true);
            var images = new List<string>();

            xml.ReadStartElement("Texture");

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "Width":
                        options.Width = xml.ReadElementContentAsInt();
                        break;
                    case "Height":
                        options.Height = xml.ReadElementContentAsInt();
                        break;
                    case "Format":
                        options.Format = TextureImporter.ParseTextureFormat(xml.ReadElementContentAsString());
                        break;
                    case "Flags":
                        options.Flags = xml.ReadElementContentAsEnum<TextureFlags>();
                        break;
                    case "GunkFlags":
                        options.GunkFlags = xml.ReadElementContentAsEnum<Akira.GunkFlags>();
                        break;
                    case "EnvMap":
                        options.EnvironmentMap = xml.ReadElementContentAsString();
                        break;
                    case "Speed":
                        options.Speed = xml.ReadElementContentAsInt();
                        break;
                    case "Image":
                        images.Add(Path.Combine(basePath, xml.ReadElementContentAsString()));
                        break;
                    default:
                        Console.Error.WriteLine("Unknown texture option {0}", xml.LocalName);
                        xml.Skip();
                        break;
                }
            }

            xml.ReadEndElement();

            options.Images = images.ToArray();
        }

        public void Write()
        {
            Parallel.ForEach(textures.Values, options =>
            {
                if (options.Images.Length > 0)
                {
                    var writer = new TexImporter(this, options);
                    writer.Import();
                    writer.Write(outputDirPath);
                }
            });

            Console.WriteLine("Imported {0} textures", textures.Count);
        }

        private class TexImporter : Importer
        {
            private readonly TextureImporter3 importer;
            private readonly TextureImporterOptions options;

            public TexImporter(TextureImporter3 importer, TextureImporterOptions options)
            {
                this.importer = importer;
                this.options = options;

                BeginImport();
            }

            public void Import()
            {
                var surfaces = new List<Surface>();

                foreach (string imageFilePath in options.Images)
                    surfaces.Add(TextureUtils.LoadImage(imageFilePath));

                if (surfaces.Count == 0)
                    throw new InvalidDataException("No images found. A texture must have at least one image.");

                TextureFormat format;

                if (options.Format != null)
                {
                    format = options.Format.Value;
                }
                else
                {
                    var surface = surfaces[0];

                    if (surface.HasTransparentPixels())
                        format = importer.defaultAlphaFormat;
                    else if (surface.Width % 4 == 0 && surface.Height % 4 == 0)
                        format = importer.defaultSquareFormat;
                    else
                        format = importer.defaultFormat;
                }

                int imageWidth = 0;
                int imageHeight = 0;

                foreach (var surface in surfaces)
                {
                    if (imageWidth == 0)
                        imageWidth = surface.Width;
                    else if (imageWidth != surface.Width)
                        throw new NotSupportedException("All animation frames must have the same size.");

                    if (imageHeight == 0)
                        imageHeight = surface.Height;
                    else if (imageHeight != surface.Height)
                        throw new NotSupportedException("All animation frames must have the same size.");
                }

                int width = options.Width;
                int height = options.Height;

                if (width == 0)
                    width = imageWidth;
                else if (width > imageWidth)
                    throw new NotSupportedException("Cannot upscale images.");

                if (height == 0)
                    height = imageHeight;
                else if (height > imageHeight)
                    throw new NotSupportedException("Cannot upscale images.");

                if (width > importer.maxSize || height > importer.maxSize)
                {
                    if (width > height)
                    {
                        height = importer.maxSize * height / width;
                        width = importer.maxSize;
                    }
                    else
                    {
                        width = importer.maxSize * width / height;
                        height = importer.maxSize;
                    }
                }

                width = TextureUtils.RoundToPowerOf2(width);
                height = TextureUtils.RoundToPowerOf2(height);

                if (width != imageWidth || height != imageHeight)
                {
                    for (int i = 0; i < surfaces.Count; i++)
                        surfaces[i] = surfaces[i].Resize(width, height);
                }

                var flags = options.Flags | TextureFlags.HasMipMaps;
                flags &= ~(TextureFlags.HasEnvMap | TextureFlags.SwapBytes);
                var envMapName = options.EnvironmentMap;

                if (format != TextureFormat.RGBA)
                    flags |= TextureFlags.SwapBytes;

                if (!string.IsNullOrEmpty(envMapName))
                    flags |= TextureFlags.HasEnvMap;

                var name = options.Name;
                int speed = options.Speed;

                for (int i = 0; i < surfaces.Count; i++)
                {
                    var descriptor = CreateInstance(TemplateTag.TXMP, i == 0 ? name : null);

                    using (var writer = descriptor.OpenWrite(128))
                    {
                        writer.Write((int)flags);
                        writer.WriteUInt16(width);
                        writer.WriteUInt16(height);
                        writer.Write((int)format);

                        if (i == 0 && surfaces.Count > 1)
                            writer.WriteInstanceId(surfaces.Count);
                        else
                            writer.Write(0);

                        if (!string.IsNullOrEmpty(envMapName))
                            writer.WriteInstanceId(surfaces.Count + ((surfaces.Count > 1) ? 1 : 0));
                        else
                            writer.Write(0);

                        writer.Write(RawWriter.Align32());
                        writer.Skip(12);

                        var mainSurface = surfaces[i];

                        var levels = new List<Surface>(16);
                        levels.Add(mainSurface);

                        if ((flags & TextureFlags.HasMipMaps) != 0)
                        {
                            int mipWidth = width;
                            int mipHeight = height;

                            var surface = mainSurface;

                            while (mipWidth > 1 || mipHeight > 1)
                            {
                                mipWidth = Math.Max(mipWidth >> 1, 1);
                                mipHeight = Math.Max(mipHeight >> 1, 1);

                                surface = surface.Resize(mipWidth, mipHeight);

                                levels.Add(surface);
                            }
                        }

                        foreach (var level in levels)
                        {
                            var surface = level.Convert(format.ToSurfaceFormat());

                            RawWriter.Write(surface.Data);
                        }
                    }
                }

                if (surfaces.Count > 1)
                {
                    var txan = CreateInstance(TemplateTag.TXAN);

                    using (var writer = txan.OpenWrite(12))
                    {
                        writer.WriteInt16(speed);
                        writer.WriteInt16(speed);
                        writer.Write(0);
                        writer.Write(surfaces.Count);
                        writer.Write(0);

                        for (int i = 1; i < surfaces.Count; i++)
                            writer.WriteInstanceId(i);
                    }
                }

                if (!string.IsNullOrEmpty(envMapName))
                    CreateInstance(TemplateTag.TXMP, envMapName);
            }
        }
    }
}
