﻿using System;
using System.Collections.Generic;
using System.IO;
using Oni.Imaging;

namespace Oni.Motoko
{
    internal class TextureImporter : Importer
    {
        private static readonly int[] powersOf2 = new[] { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 };
        private static readonly Dictionary<string, TextureFormat> formatNames = new Dictionary<string, TextureFormat>(StringComparer.OrdinalIgnoreCase)
        {
            { "bgr32", TextureFormat.BGR },
            { "bgr", TextureFormat.BGR },
            { "bgra32", TextureFormat.RGBA },
            { "rgba", TextureFormat.RGBA },
            { "bgra4444", TextureFormat.BGRA4444 },
            { "bgr555", TextureFormat.BGR555 },
            { "bgra5551", TextureFormat.BGRA5551 },
            { "dxt1", TextureFormat.DXT1 }
        };

        private readonly bool allowLargeTextures;
        private readonly bool noMipMaps;
        private readonly TextureFlags defaultFlags;
        private readonly TextureFormat? defaultFormat;
        private readonly string envmapName;

        public static TextureFormat ParseTextureFormat(string name)
        {
            TextureFormat format;

            if (!formatNames.TryGetValue(name, out format))
                throw new FormatException(string.Format("Invalid texture format '{0}'", name));

            return format;
        }

        public TextureImporter(TextureImporterOptions options)
        {
            if (options != null)
            {
                defaultFormat = options.Format;
                envmapName = options.EnvironmentMap;
                defaultFlags = options.Flags & ~TextureFlags.HasMipMaps;
                noMipMaps = (options.Flags & TextureFlags.HasMipMaps) == 0;
                allowLargeTextures = true;
            }
        }

        public TextureImporter(string[] args)
        {
            foreach (string arg in args)
            {
                if (arg.StartsWith("-format:", StringComparison.Ordinal))
                {
                    TextureFormat formatArg;
                    string formatName = arg.Substring(8);

                    if (!formatNames.TryGetValue(formatName, out formatArg))
                        throw new NotSupportedException(string.Format("Unknown texture format {0}", formatName));

                    defaultFormat = formatArg;
                }
                else if (arg.StartsWith("-envmap:", StringComparison.Ordinal))
                {
                    string envmap = arg.Substring(8);

                    if (envmap.Length > 0)
                        envmapName = envmap;
                }
                else if (arg == "-large")
                {
                    allowLargeTextures = true;
                }
                else if (arg == "-nouwrap")
                {
                    defaultFlags |= TextureFlags.NoUWrap;
                }
                else if (arg == "-novwrap")
                {
                    defaultFlags |= TextureFlags.NoVWrap;
                }
                else if (arg == "-nomipmaps")
                {
                    noMipMaps = true;
                }
            }
        }

        public override void Import(string filePath, string outputDirPath)
        {
            var texture = new Texture
            {
                Name = DecodeFileName(filePath),
                Flags = defaultFlags
            };

            LoadImage(texture, filePath);

            if (envmapName != null)
            {
                texture.EnvMap = new Texture();
                texture.EnvMap.Name = envmapName;
            }

            BeginImport();
            TextureDatWriter.Write(texture, this);
            Write(outputDirPath);
        }

        private void LoadImage(Texture texture, string filePath)
        {
            var surfaces = new List<Surface>();

            switch (Path.GetExtension(filePath).ToLowerInvariant())
            {
                case ".tga":
                    surfaces.Add(TgaReader.Read(filePath));
                    break;
                case ".dds":
                    surfaces.AddRange(DdsReader.Read(filePath, noMipMaps));
                    break;
                default:
#if !NETCORE
                    surfaces.Add(SysReader.Read(filePath));
#endif
                    break;
            }

            if (surfaces.Count == 0)
                throw new InvalidDataException(string.Format("Could not load image '{0}'", filePath));

            var mainSurface = surfaces[0];

            if (Array.IndexOf(powersOf2, mainSurface.Width) == -1)
                Console.Error.WriteLine("Warning: Texture '{0}' width is not a power of 2", filePath);

            if (Array.IndexOf(powersOf2, mainSurface.Height) == -1)
                Console.Error.WriteLine("Warning: Texture '{0}' height is not a power of 2", filePath);

            if (surfaces.Count == 1)
            {
                mainSurface.CleanupAlpha();
            }

            if (mainSurface.Format == SurfaceFormat.DXT1 && defaultFormat != null && defaultFormat != TextureFormat.DXT1)
            {
                //
                // If the source image is DXT1 but the target is not then we can convert here
                // to a more flexible image format.
                //

                for (int i = 0; i < surfaces.Count; i++)
                    surfaces[i] = surfaces[i].Convert(SurfaceFormat.RGBA);

                mainSurface = surfaces[0];
            }

            if (!allowLargeTextures && (mainSurface.Width > 256 || mainSurface.Height > 256))
            {
                if (surfaces.Count == 1)
                {
                    int sx = mainSurface.Width / 256;
                    int sy = mainSurface.Height / 256;

                    int s = Math.Max(sx, sy);

                    mainSurface = mainSurface.Resize(mainSurface.Width / s, mainSurface.Height / s);
                    surfaces[0] = mainSurface;
                }
                else
                {
                    while (surfaces.Count > 0 && (surfaces[0].Width > 256 || surfaces[0].Height > 256))
                        surfaces.RemoveAt(0);

                    mainSurface = surfaces[0];
                }
            }

            if (surfaces.Count == 1 && !noMipMaps && mainSurface.Format != SurfaceFormat.DXT1)
            {
                var surface = mainSurface;

                while (surface.Width > 1 || surface.Height > 1)
                {
                    int width = Math.Max(surface.Width >> 1, 1);
                    int height = Math.Max(surface.Height >> 1, 1);

                    surface = surface.Resize(width, height);

                    surfaces.Add(surface);
                }
            }

            var convertToFormat = mainSurface.Format;

            if (defaultFormat != null)
            {
                texture.Format = defaultFormat.Value;
                convertToFormat = texture.Format.ToSurfaceFormat();
            }
            else
            {
                switch (mainSurface.Format)
                {
                    case SurfaceFormat.BGRA4444:
                        texture.Format = TextureFormat.BGRA4444;
                        break;

                    case SurfaceFormat.BGRX5551:
                        texture.Format = TextureFormat.BGR555;
                        break;

                    case SurfaceFormat.BGR565:
                        texture.Format = TextureFormat.BGR555;
                        break;

                    case SurfaceFormat.BGRA5551:
                        texture.Format = TextureFormat.BGRA5551;
                        break;

                    case SurfaceFormat.BGRX:
                    case SurfaceFormat.RGBX:
                        texture.Format = TextureFormat.BGR;
                        convertToFormat = SurfaceFormat.BGRX;
                        break;

                    case SurfaceFormat.BGRA:
                    case SurfaceFormat.RGBA:
                        //
                        // We don't do RGBA32 unless specifically required because
                        // Oni needs a patch to support it.
                        //
                        texture.Format = TextureFormat.BGRA4444;
                        convertToFormat = SurfaceFormat.BGRA4444;
                        break;

                    case SurfaceFormat.DXT1:
                        texture.Format = TextureFormat.DXT1;
                        break;

                    default:
                        throw new NotSupportedException(string.Format("Image format {0} cannot be imported", mainSurface.Format));
                }
            }

            if (convertToFormat != mainSurface.Format)
            {
                for (int i = 0; i < surfaces.Count; i++)
                    surfaces[i] = surfaces[i].Convert(convertToFormat);

                mainSurface = surfaces[0];
            }

            if (texture.Format != TextureFormat.RGBA)
                texture.Flags |= TextureFlags.SwapBytes;

            texture.Width = mainSurface.Width;
            texture.Height = mainSurface.Height;
            texture.Surfaces.AddRange(surfaces);
        }
    }
}
