using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace ChillyRoom.UnityEditor.iOS.Xcode
{
	internal class DeviceTypeRequirement
    {
        public static readonly string Key = "idiom";
        public static readonly string Any = "universal";
        public static readonly string iPhone = "iphone";
        public static readonly string iPad = "ipad";
        public static readonly string Mac = "mac";
        public static readonly string iWatch = "watch";
    }

	internal class MemoryRequirement
    {
        public static readonly string Key = "memory";
        public static readonly string Any = "";
        public static readonly string Mem1GB = "1GB";
        public static readonly string Mem2GB = "2GB";
    }

	internal class GraphicsRequirement
    {
        public static readonly string Key = "graphics-feature-set";
        public static readonly string Any = "";
        public static readonly string Metal1v2 = "metal1v2";
        public static readonly string Metal2v2 = "metal2v2";
    }

    // only used for image sets
    internal class SizeClassRequirement
    {
        public static readonly string HeightKey = "height-class";
        public static readonly string WidthKey = "width-class";
        public static readonly string Any = "";
        public static readonly string Compact = "compact";
        public static readonly string Regular = "regular";
    }

    // only used for image sets
    internal class ScaleRequirement
    {
        public static readonly string Key = "scale";
        public static readonly string Any = ""; // vector image
        public static readonly string X1 = "1x";
        public static readonly string X2 = "2x";
        public static readonly string X3 = "3x";
    }

    internal class DeviceRequirement
    {
        internal Dictionary<string, string> values = new Dictionary<string, string>();

        public DeviceRequirement AddDevice(string device)
        {
			AddCustom(DeviceTypeRequirement.Key, device);
            return this;
        }

        public DeviceRequirement AddMemory(string memory)
        {
			AddCustom(MemoryRequirement.Key, memory);
            return this;
        }

        public DeviceRequirement AddGraphics(string graphics)
        {
			AddCustom(GraphicsRequirement.Key, graphics);
            return this;
        }

        public DeviceRequirement AddWidthClass(string sizeClass)
        {
            AddCustom(SizeClassRequirement.WidthKey, sizeClass);
            return this;
        }

        public DeviceRequirement AddHeightClass(string sizeClass)
        {
            AddCustom(SizeClassRequirement.HeightKey, sizeClass);
            return this;
        }

        public DeviceRequirement AddScale(string scale)
        {
            AddCustom(ScaleRequirement.Key, scale);
            return this;
        }

        public DeviceRequirement AddCustom(string key, string value)
        {
            if (values.ContainsKey(key))
                values.Remove(key);
            values.Add(key, value);
            return this;
        }

        public DeviceRequirement()
        {
			values.Add("idiom", DeviceTypeRequirement.Any);
        }
    }

    internal class AssetCatalog
    {
        AssetFolder m_Root;

        public string path { get { return m_Root.path; } }
        public AssetFolder root { get { return m_Root; } }

        public AssetCatalog(string path, string authorId)
        {
            if (Path.GetExtension(path) != ".xcassets")
                throw new Exception("Asset catalogs must have xcassets extension");
            m_Root = new AssetFolder(path, null, authorId);
        }

        AssetFolder OpenFolderForResource(string relativePath)
        {
            var pathItems = PBXPath.Split(relativePath).ToList();

            // remove path filename
            pathItems.RemoveAt(pathItems.Count - 1);

            AssetFolder folder = root;
            foreach (var pathItem in pathItems)
                folder = folder.OpenFolder(pathItem);
            return folder;
        }

        // Checks if a dataset at the given path exists and returns it if it does.
        // Otherwise, creates a new dataset. Parent folders are created if needed.
        // Note: the path is filesystem path, not logical asset name formed
        // only from names of the folders that have "provides namespace" attribute.
        // If you want to put certain resources in folders with namespace, first
        // manually create the folders and then set the providesNamespace attribute.
        // OpenNamespacedFolder may help to do this.
        public AssetDataSet OpenDataSet(string relativePath)
        {
            var folder = OpenFolderForResource(relativePath);
            return folder.OpenDataSet(Path.GetFileName(relativePath));
        }

        public AssetImageSet OpenImageSet(string relativePath)
        {
            var folder = OpenFolderForResource(relativePath);
            return folder.OpenImageSet(Path.GetFileName(relativePath));
        }
        
        public AssetImageStack OpenImageStack(string relativePath)
        {
            var folder = OpenFolderForResource(relativePath);
            return folder.OpenImageStack(Path.GetFileName(relativePath));
        }

        public AssetBrandAssetGroup OpenBrandAssetGroup(string relativePath)
        {
            var folder = OpenFolderForResource(relativePath);
            return folder.OpenBrandAssetGroup(Path.GetFileName(relativePath));
        }

        // Checks if a folder with given path exists and returns it if it does.
        // Otherwise, creates a new folder. Parent folders are created if needed.
        public AssetFolder OpenFolder(string relativePath)
        {
            if (relativePath == null)
                return root;
            var pathItems = PBXPath.Split(relativePath);
            if (pathItems.Length == 0)
                return root;
            AssetFolder folder = root;
            foreach (var pathItem in pathItems)
                folder = folder.OpenFolder(pathItem);
            return folder;
        }

        // Creates a directory structure with "provides namespace" attribute.
        // First, retrieves or creates the directory at relativeBasePath, creating parent
        // directories if needed. Effectively calls OpenFolder(relativeBasePath).
        // Then, relative to this directory, creates namespacePath directories with "provides
        // namespace" attribute set. Fails if the attribute can't be set.
        public AssetFolder OpenNamespacedFolder(string relativeBasePath, string namespacePath)
        {
            var folder = OpenFolder(relativeBasePath);
            var pathItems = PBXPath.Split(namespacePath);
            foreach (var pathItem in pathItems)
            {
                folder = folder.OpenFolder(pathItem);
                folder.providesNamespace = true;
            }
            return folder;
        }

        public void Write()
        {
            Write(null);
        }

        public void Write(List<string> warnings)
        {
            m_Root.Write(warnings);
        }
    }

    internal abstract class AssetCatalogItem
    {
        public readonly string name;
        public readonly string authorId;
        public string path { get { return m_Path; } }

        protected Dictionary<string, string> m_Properties = new Dictionary<string, string>();

        protected string m_Path;

        public AssetCatalogItem(string name, string authorId)
        {
            if (name != null && name.Contains("/"))
                throw new Exception("Asset catalog item must not have slashes in name");
            this.name = name;
            this.authorId = authorId;
        }

        protected JsonElementDict WriteInfoToJson(JsonDocument doc)
        {
            var info = doc.root.CreateDict("info");
            info.SetInteger("version", 1);
            info.SetString("author", authorId);
            return info;
        }

        public abstract void Write(List<string> warnings);
    }

    internal class AssetFolder : AssetCatalogItem
    {
        List<AssetCatalogItem> m_Items = new List<AssetCatalogItem>();
        bool m_ProvidesNamespace = false;

        public bool providesNamespace
        {
            get { return m_ProvidesNamespace; }
            set {
                if (m_Items.Count > 0 && value != m_ProvidesNamespace)
                    throw new Exception("Asset folder namespace providing status can't be "+
                                        "changed after items have been added");
                m_ProvidesNamespace = value;
            }
        }

        internal AssetFolder(string parentPath, string name, string authorId) : base(name, authorId)
        {
            if (name != null)
                m_Path = Path.Combine(parentPath, name);
            else
                m_Path = parentPath;
        }

        // Checks if a folder with given name exists and returns it if it does.
        // Otherwise, creates a new folder.
        public AssetFolder OpenFolder(string name)
        {
            var item = GetChild(name);
            if (item != null)
            {
                if (item is AssetFolder)
                    return item as AssetFolder;
                throw new Exception("The given path is already occupied with an asset");
            }

            var folder = new AssetFolder(m_Path, name, authorId);
            m_Items.Add(folder);
            return folder;
        }

        T GetExistingItemWithType<T>(string name) where T : class
        {
            var item = GetChild(name);
            if (item != null)
            {
                if (item is T)
                    return item as T;
                throw new Exception("The given path is already occupied with an asset");
            }
            return null;
        }

        // Checks if a dataset with given name exists and returns it if it does.
        // Otherwise, creates a new data set.
        public AssetDataSet OpenDataSet(string name)
        {
            var item = GetExistingItemWithType<AssetDataSet>(name);
            if (item != null)
                return item;

            var dataset = new AssetDataSet(m_Path, name, authorId);
            m_Items.Add(dataset);
            return dataset;
        }

        // Checks if an imageset with given name exists and returns it if it does.
        // Otherwise, creates a new image set.
        public AssetImageSet OpenImageSet(string name)
        {
            var item = GetExistingItemWithType<AssetImageSet>(name);
            if (item != null)
                return item;

            var imageset = new AssetImageSet(m_Path, name, authorId);
            m_Items.Add(imageset);
            return imageset;
        }
        
        // Checks if a image stack with given name exists and returns it if it does.
        // Otherwise, creates a new image stack.
        public AssetImageStack OpenImageStack(string name)
        {
            var item = GetExistingItemWithType<AssetImageStack>(name);
            if (item != null)
                return item;
            
            var imageStack = new AssetImageStack(m_Path, name, authorId);
            m_Items.Add(imageStack);
            return imageStack;
        }
        
        // Checks if a brand asset with given name exists and returns it if it does.
        // Otherwise, creates a new brand asset.
        public AssetBrandAssetGroup OpenBrandAssetGroup(string name)
        {
            var item = GetExistingItemWithType<AssetBrandAssetGroup>(name);
            if (item != null)
                return item;
            
            var brandAsset = new AssetBrandAssetGroup(m_Path, name, authorId);
            m_Items.Add(brandAsset);
            return brandAsset;
        }

        // Returns the requested item or null if not found
        public AssetCatalogItem GetChild(string name)
        {
            foreach (var item in m_Items)
            {
                if (item.name == name)
                    return item;
            }
            return null;
        }

        void WriteJson()
        {
            if (!providesNamespace)
                return; // json is optional when namespace is not provided

            var doc = new JsonDocument();

            WriteInfoToJson(doc);

            var props = doc.root.CreateDict("properties");
            props.SetBoolean("provides-namespace", providesNamespace);
            doc.WriteToFile(Path.Combine(m_Path, "Contents.json"));
        }

        public override void Write(List<string> warnings)
        {
            if (Directory.Exists(m_Path))
                Directory.Delete(m_Path, true); // ensure we start from clean state
            Directory.CreateDirectory(m_Path);
            WriteJson();

            foreach (var item in m_Items)
                item.Write(warnings);
        }
    }

    abstract class AssetCatalogItemWithVariants : AssetCatalogItem
    {
		protected List<VariantData> m_Variants = new List<VariantData>();
        protected List<string> m_ODRTags = new List<string>();

        protected AssetCatalogItemWithVariants(string name, string authorId) :
            base(name, authorId)
        {
        }

        protected class VariantData
        {
			public DeviceRequirement requirement;
            public string path;

            public VariantData(DeviceRequirement requirement, string path)
            {
				this.requirement = requirement;
                this.path = path;
            }
        }

        public bool HasVariant(DeviceRequirement requirement)
        {
            foreach (var item in m_Variants)
            {
                if (item.requirement.values == requirement.values)
                    return true;
            }
            return false;
        }

        public void AddOnDemandResourceTag(string tag)
        {
            if (!m_ODRTags.Contains(tag))
                m_ODRTags.Add(tag);
        }

        protected void AddVariant(VariantData newItem)
        {
            foreach (var item in m_Variants)
            {
                if (item.requirement.values == newItem.requirement.values)
                    throw new Exception("The given requirement has been already added");
                if (Path.GetFileName(item.path) == Path.GetFileName(path))
                    throw new Exception("Two items within the same set must not have the same file name");
            }
            if (Path.GetFileName(newItem.path) == "Contents.json")
                throw new Exception("The file name must not be equal to Contents.json");
            m_Variants.Add(newItem);
        }

        protected void WriteODRTagsToJson(JsonElementDict info)
        {
            if (m_ODRTags.Count > 0)
            {
                var tags = info.CreateArray("on-demand-resource-tags");
                foreach (var tag in m_ODRTags)
                    tags.AddString(tag);
            }
        }

        protected void WriteRequirementsToJson(JsonElementDict item, DeviceRequirement req)
        {
            foreach (var kv in req.values)
            {
                if (kv.Value != null && kv.Value != "")
                    item.SetString(kv.Key, kv.Value);
            }
        }
    }

    internal class AssetDataSet : AssetCatalogItemWithVariants
    {
        class DataSetVariant : VariantData
        {
            public string id;

            public DataSetVariant(DeviceRequirement requirement, string path, string id) : base(requirement, path)
            {
                this.id = id;
            }
        }

        internal AssetDataSet(string parentPath, string name, string authorId) : base(name, authorId)
        {
            m_Path = Path.Combine(parentPath, name + ".dataset");
        }

		// an exception is thrown is two equivalent requirements are added.
        // The same asset dataset must not have paths with equivalent filenames.
        // The identifier allows to identify which data variant is actually loaded (use
        // the typeIdentifer property of the NSDataAsset that was created from the data set)
        public void AddVariant(DeviceRequirement requirement, string path, string typeIdentifier)
        {
            foreach (DataSetVariant item in m_Variants)
            {
                if (item.id != null && typeIdentifier != null && item.id == typeIdentifier)
                    throw new Exception("Two items within the same dataset must not have the same id");
            }
            AddVariant(new DataSetVariant(requirement, path, typeIdentifier));
        }

        public override void Write(List<string> warnings)
        {
            Directory.CreateDirectory(m_Path);

            var doc = new JsonDocument();

            var info = WriteInfoToJson(doc);
            WriteODRTagsToJson(info);

            var data = doc.root.CreateArray("data");

            foreach (DataSetVariant item in m_Variants)
            {
                var filename = Path.GetFileName(item.path);
                if (!File.Exists(item.path))
                {
                    if (warnings != null)
                        warnings.Add("File not found: " + item.path);
                }
                else
                    File.Copy(item.path, Path.Combine(m_Path, filename));

                var docItem = data.AddDict();
                docItem.SetString("filename", filename);
                WriteRequirementsToJson(docItem, item.requirement);
                if (item.id != null)
                    docItem.SetString("universal-type-identifier", item.id);
            }
            doc.WriteToFile(Path.Combine(m_Path, "Contents.json"));
        }
    }

    internal class ImageAlignment
    {
        public int left = 0, right = 0, top = 0, bottom = 0;
    }

    internal class ImageResizing
    {
        public enum SlicingType
        {
            Horizontal,
            Vertical,
            HorizontalAndVertical
        }

        public enum ResizeMode
        {
            Stretch,
            Tile
        }

        public SlicingType type = SlicingType.HorizontalAndVertical;
        public int left = 0;                // only valid for horizontal slicing
        public int right = 0;               // only valid for horizontal slicing
        public int top = 0;                 // only valid for vertical slicing
        public int bottom = 0;              // only valid for vertical slicing
        public ResizeMode centerResizeMode = ResizeMode.Stretch;
        public int centerWidth = 0;         // only valid for vertical slicing
        public int centerHeight = 0;        // only valid for horizontal slicing
    }

    // TODO: rendering intent property
    internal class AssetImageSet : AssetCatalogItemWithVariants
    {
        internal AssetImageSet(string assetCatalogPath, string name, string authorId) : base(name, authorId)
        {
            m_Path = Path.Combine(assetCatalogPath, name + ".imageset");
        }

        class ImageSetVariant : VariantData
        {
            public ImageAlignment alignment = null;
            public ImageResizing resizing = null;

            public ImageSetVariant(DeviceRequirement requirement, string path) : base(requirement, path)
            {
            }
        }

        public void AddVariant(DeviceRequirement requirement, string path)
        {
            AddVariant(new ImageSetVariant(requirement, path));
        }

        public void AddVariant(DeviceRequirement requirement, string path, ImageAlignment alignment, ImageResizing resizing)
        {
            var imageset = new ImageSetVariant(requirement, path);
            imageset.alignment = alignment;
            imageset.resizing = resizing;
            AddVariant(imageset);
        }

        void WriteAlignmentToJson(JsonElementDict item, ImageAlignment alignment)
        {
            var docAlignment = item.CreateDict("alignment-insets");
            docAlignment.SetInteger("top", alignment.top);
            docAlignment.SetInteger("bottom", alignment.bottom);
            docAlignment.SetInteger("left", alignment.left);
            docAlignment.SetInteger("right", alignment.right);
        }

        static string GetSlicingMode(ImageResizing.SlicingType mode)
        {
            switch (mode)
            {
                case ImageResizing.SlicingType.Horizontal: return "3-part-horizontal";
                case ImageResizing.SlicingType.Vertical: return "3-part-vertical";
                case ImageResizing.SlicingType.HorizontalAndVertical: return "9-part";
            }
            return "";
        }

        static string GetCenterResizeMode(ImageResizing.ResizeMode mode)
        {
            switch (mode)
            {
                case ImageResizing.ResizeMode.Stretch: return "stretch";
                case ImageResizing.ResizeMode.Tile: return "tile";
            }
            return "";
        }

        void WriteResizingToJson(JsonElementDict item, ImageResizing resizing)
        {
            var docResizing = item.CreateDict("resizing");
            docResizing.SetString("mode", GetSlicingMode(resizing.type));

            var docCenter = docResizing.CreateDict("center");
            docCenter.SetString("mode", GetCenterResizeMode(resizing.centerResizeMode));
            docCenter.SetInteger("width", resizing.centerWidth);
            docCenter.SetInteger("height", resizing.centerHeight);

            var docInsets = docResizing.CreateDict("cap-insets");
            docInsets.SetInteger("top", resizing.top);
            docInsets.SetInteger("bottom", resizing.bottom);
            docInsets.SetInteger("left", resizing.left);
            docInsets.SetInteger("right", resizing.right);
        }

        public override void Write(List<string> warnings)
        {
            Directory.CreateDirectory(m_Path);
            var doc = new JsonDocument();
            var info = WriteInfoToJson(doc);
            WriteODRTagsToJson(info);

            var images = doc.root.CreateArray("images");

            foreach (ImageSetVariant item in m_Variants)
            {
                var filename = Path.GetFileName(item.path);
                if (!File.Exists(item.path))
                {
                    if (warnings != null)
                        warnings.Add("File not found: " + item.path);
                }
                else
                    File.Copy(item.path, Path.Combine(m_Path, filename));

                var docItem = images.AddDict();
                docItem.SetString("filename", filename);
                WriteRequirementsToJson(docItem, item.requirement);
                if (item.alignment != null)
                    WriteAlignmentToJson(docItem, item.alignment);
                if (item.resizing != null)
                    WriteResizingToJson(docItem, item.resizing);
            }
            doc.WriteToFile(Path.Combine(m_Path, "Contents.json"));
        }
    }

    /*  A stack layer may either contain an image set or reference another imageset
    */
    class AssetImageStackLayer : AssetCatalogItem
    {
        internal AssetImageStackLayer(string assetCatalogPath, string name, string authorId) : base(name, authorId)
        {
            m_Path = Path.Combine(assetCatalogPath, name + ".imagestacklayer");
            m_Imageset = new AssetImageSet(m_Path, "Content", authorId);
        }

        AssetImageSet m_Imageset = null;
        string m_ReferencedName = null;

        public void SetReference(string name)
        {
            m_Imageset = null;
            m_ReferencedName = name;
        }

        public string ReferencedName()
        {
            return m_ReferencedName;
        }

        public AssetImageSet GetImageSet()
        {
            return m_Imageset;
        }

        public override void Write(List<string> warnings)
        {
            Directory.CreateDirectory(m_Path);
            var doc = new JsonDocument();
            WriteInfoToJson(doc);

            if (m_ReferencedName != null)
            {
                var props = doc.root.CreateDict("properties");
                var reference = props.CreateDict("content-reference");
                reference.SetString("type", "image-set");
                reference.SetString("name", m_ReferencedName);
                reference.SetString("matching-style", "fully-qualified-name");
            }
            if (m_Imageset != null)
                m_Imageset.Write(warnings);

            doc.WriteToFile(Path.Combine(m_Path, "Contents.json"));
        }
    }

    class AssetImageStack : AssetCatalogItem
    {
        List<AssetImageStackLayer> m_Layers = new List<AssetImageStackLayer>();

        internal AssetImageStack(string assetCatalogPath, string name, string authorId) : base(name, authorId)
        {
            m_Path = Path.Combine(assetCatalogPath, name + ".imagestack");
        }

        public AssetImageStackLayer AddLayer(string name)
        {
            foreach (var layer in m_Layers)
            {
                if (layer.name == name)
                    throw new Exception("A layer with given name already exists");
            }
            var newLayer = new AssetImageStackLayer(m_Path, name, authorId);
            m_Layers.Add(newLayer);
            return newLayer;
        }

        public override void Write(List<string> warnings)
        {
            Directory.CreateDirectory(m_Path);
            var doc = new JsonDocument();
            WriteInfoToJson(doc);

            var docLayers = doc.root.CreateArray("layers");
            foreach (var layer in m_Layers)
            {
                layer.Write(warnings);
 
                var docLayer = docLayers.AddDict();
                docLayer.SetString("filename", Path.GetFileName(layer.path));
            }
            doc.WriteToFile(Path.Combine(m_Path, "Contents.json"));
        }
    }
    
    class AssetBrandAssetGroup : AssetCatalogItem
    {
        class AssetBrandAssetItem
        {
            internal string idiom = null;
            internal string role = null;
            internal int width, height;
            internal AssetCatalogItem item = null;
            
        }

        List<AssetBrandAssetItem> m_Items = new List<AssetBrandAssetItem>();
        
        internal AssetBrandAssetGroup(string assetCatalogPath, string name, string authorId) : base(name, authorId)
        {
            m_Path = Path.Combine(assetCatalogPath, name + ".brandassets");
        }
        
        void AddItem(AssetCatalogItem item, string idiom, string role, int width, int height)
        {
            foreach (var it in m_Items)
            {
                if (it.item.name == item.name)
                    throw new Exception("An item with given name already exists");
            }
            var newItem = new AssetBrandAssetItem();
            newItem.item = item;
            newItem.idiom = idiom;
            newItem.role = role;
            newItem.width = width;
            newItem.height = height;
            m_Items.Add(newItem);
        }

        public AssetImageSet OpenImageSet(string name, string idiom, string role, int width, int height)
        {
            var newItem = new AssetImageSet(m_Path, name, authorId);
            AddItem(newItem, idiom, role, width, height);
            return newItem;
        }

        public AssetImageStack OpenImageStack(string name, string idiom, string role, int width, int height)
        {
            var newItem = new AssetImageStack(m_Path, name, authorId);
            AddItem(newItem, idiom, role, width, height);
            return newItem;
        }
        
        public override void Write(List<string> warnings)
        {
            Directory.CreateDirectory(m_Path);
            var doc = new JsonDocument();
            WriteInfoToJson(doc);
            
            var docAssets = doc.root.CreateArray("assets");
            foreach (var item in m_Items)
            {
                var docAsset = docAssets.AddDict();
                docAsset.SetString("size", String.Format("{0}x{1}", item.width, item.height));
                docAsset.SetString("idiom", item.idiom);
                docAsset.SetString("role", item.role);
                docAsset.SetString("filename", Path.GetFileName(item.item.path));
                
                item.item.Write(warnings);
            }
            doc.WriteToFile(Path.Combine(m_Path, "Contents.json"));
        }
    }

} // namespace UnityEditor.iOS.Xcode
