using Aseprite.Chunks;
using Aseprite.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
namespace Aseprite
{
    // See file specs here: https://github.com/aseprite/aseprite/blob/master/docs/ase-file-specs.md
    public class AseFile
    {
        public Header Header { get; private set; }
        public List Frames { get; private set; }
        private Dictionary chunkCache = new Dictionary();
        public AseFile(Stream stream)
        {
            BinaryReader reader = new BinaryReader(stream);
            byte[] header = reader.ReadBytes(128);
            Header = new Header(header);
            Frames = new List();
            while (reader.BaseStream.Position < reader.BaseStream.Length)
            {
                Frames.Add(new Frame(this, reader));
            }
        }
        public List GetChunks() where T : Chunk
        {
            List chunks = new List();
            for (int i = 0; i < this.Frames.Count; i++)
            {
                List cs = this.Frames[i].GetChunks();
                chunks.AddRange(cs);
            }
            return chunks;
        }
        public T GetChunk() where T : Chunk
        {
            if (!chunkCache.ContainsKey(typeof(T)))
            {
                for (int i = 0; i < this.Frames.Count; i++)
                {
                    List cs = this.Frames[i].GetChunks();
                    if (cs.Count > 0)
                    {
                        chunkCache.Add(typeof(T), cs[0]);
                        break;
                    }
                }
            }
            if (chunkCache.ContainsKey(typeof(T)))
            {
                return (T)chunkCache[typeof(T)];
            }
            else
            {
                return null;
            }
        }
        public Texture2D[] GetFrames()
        {
            List frames = new List();
            for (int i = 0; i < Frames.Count; i++)
            {
                frames.Add(GetFrame(i));
            }
            return frames.ToArray();
        }
        public Texture2D[] GetLayersAsFrames()
        {
            List frames = new List();
            List layers = GetChunks();
            for (int i = 0; i < layers.Count; i++)
            {
                List layerFrames = GetLayerTexture(i, layers[i]);
                if (layerFrames.Count > 0)
                    frames.AddRange(layerFrames);
            }
            return frames.ToArray();
        }
        private LayerChunk GetParentLayer(LayerChunk layer)
        {
            if (layer.LayerChildLevel == 0)
                return null;
            int childLevel = layer.LayerChildLevel;
            List layers = GetChunks();
            int index = layers.IndexOf(layer);
            if (index < 0)
                return null;
            for (int i = index - 1; i > 0; i--)
            {
                if (layers[i].LayerChildLevel == layer.LayerChildLevel - 1)
                    return layers[i];
            }
            return null;
        }
        public List GetLayerTexture(int layerIndex, LayerChunk layer)
        {
            List layers = GetChunks();
            List textures = new List();
            for (int frameIndex = 0; frameIndex < Frames.Count; frameIndex++)
            {
                Frame frame = Frames[frameIndex];
                List cels = frame.GetChunks();
                for (int i = 0; i < cels.Count; i++)
                {
                    if (cels[i].LayerIndex != layerIndex)
                        continue;
                    LayerBlendMode blendMode = layer.BlendMode;
                    float opacity = Mathf.Min(layer.Opacity / 255f, cels[i].Opacity / 255f);
                    bool visibility = layer.Visible;
                    LayerChunk parent = GetParentLayer(layer);
                    while (parent != null)
                    {
                        visibility &= parent.Visible;
                        if (visibility == false)
                            break;
                        parent = GetParentLayer(parent);
                    }
                    if (visibility == false || layer.LayerType == LayerType.Group)
                        continue;
                    textures.Add(GetTextureFromCel(cels[i]));
                }
            }
            return textures;
        }
        public Texture2D GetFrame(int index)
        {
            Frame frame = Frames[index];
            Texture2D texture = Texture2DUtil.CreateTransparentTexture(Header.Width, Header.Height);
            List layers = GetChunks();
            List cels = frame.GetChunks();
            cels.Sort((ca, cb) => ca.LayerIndex.CompareTo(cb.LayerIndex));
            for (int i = 0; i < cels.Count; i++)
            {
                LayerChunk layer = layers[cels[i].LayerIndex];
                if (layer.LayerName.StartsWith("@")) //ignore metadata layer
                    continue;
                LayerBlendMode blendMode = layer.BlendMode;
                float opacity = Mathf.Min(layer.Opacity / 255f, cels[i].Opacity / 255f);
                bool visibility = layer.Visible;
                LayerChunk parent = GetParentLayer(layer);
                while (parent != null)
                {
                    visibility &= parent.Visible;
                    if (visibility == false)
                        break;
                    parent = GetParentLayer(parent);
                }
                if (visibility == false || layer.LayerType == LayerType.Group)
                    continue;
                Texture2D celTex = GetTextureFromCel(cels[i]);
                switch (blendMode)
                {
                    case LayerBlendMode.Normal: texture = Texture2DBlender.Normal(texture, celTex, opacity); break;
                    case LayerBlendMode.Multiply: texture = Texture2DBlender.Multiply(texture, celTex, opacity); break;
                    case LayerBlendMode.Screen: texture = Texture2DBlender.Screen(texture, celTex); break;
                    case LayerBlendMode.Overlay: texture = Texture2DBlender.Overlay(texture, celTex); break;
                    case LayerBlendMode.Darken: texture = Texture2DBlender.Darken(texture, celTex); break;
                    case LayerBlendMode.Lighten: texture = Texture2DBlender.Lighten(texture, celTex); break;
                    case LayerBlendMode.ColorDodge: texture = Texture2DBlender.ColorDodge(texture, celTex); break;
                    case LayerBlendMode.ColorBurn: texture = Texture2DBlender.ColorBurn(texture, celTex); break;
                    case LayerBlendMode.HardLight: texture = Texture2DBlender.HardLight(texture, celTex); break;
                    case LayerBlendMode.SoftLight: texture = Texture2DBlender.SoftLight(texture, celTex); break;
                    case LayerBlendMode.Difference: texture = Texture2DBlender.Difference(texture, celTex); break;
                    case LayerBlendMode.Exclusion: texture = Texture2DBlender.Exclusion(texture, celTex); break;
                    case LayerBlendMode.Hue: texture = Texture2DBlender.Hue(texture, celTex); break;
                    case LayerBlendMode.Saturation: texture = Texture2DBlender.Saturation(texture, celTex); break;
                    case LayerBlendMode.Color: texture = Texture2DBlender.Color(texture, celTex); break;
                    case LayerBlendMode.Luminosity: texture = Texture2DBlender.Luminosity(texture, celTex); break;
                    case LayerBlendMode.Addition: texture = Texture2DBlender.Addition(texture, celTex); break;
                    case LayerBlendMode.Subtract: texture = Texture2DBlender.Subtract(texture, celTex); break;
                    case LayerBlendMode.Divide: texture = Texture2DBlender.Divide(texture, celTex); break;
                }
            }
            return texture;
        }
        public Texture2D GetTextureFromCel(CelChunk cel)
        {
            int canvasWidth = Header.Width;
            int canvasHeight = Header.Height;
            Texture2D texture = Texture2DUtil.CreateTransparentTexture(canvasWidth, canvasHeight);
            Color[] colors = new Color[canvasWidth * canvasHeight];
            int pixelIndex = 0;
            int celXEnd = cel.Width + cel.X;
            int celYEnd = cel.Height + cel.Y;
            for (int y = cel.Y; y < celYEnd; y++)
            {
                if (y < 0 || y >= canvasHeight)
                {
                    pixelIndex += cel.Width;
                    continue;
                }
                for (int x = cel.X; x < celXEnd; x++)
                {
                    if (x >= 0 && x < canvasWidth)
                    {
                        int index = (canvasHeight - 1 - y) * canvasWidth + x;
                        colors[index] = cel.RawPixelData[pixelIndex].GetColor();
                    }
                    ++pixelIndex;
                }
            }
            texture.SetPixels(0, 0, canvasWidth, canvasHeight, colors);
            texture.Apply();
            return texture;
        }
        public FrameTag[] GetAnimations()
        {
            List tagChunks = this.GetChunks();
            List animations = new List();
            foreach (FrameTagsChunk tagChunk in tagChunks)
            {
                foreach (FrameTag tag in tagChunk.Tags)
                {
                    animations.Add(tag);
                }
            }
            return animations.ToArray();
        }
        public MetaData[] GetMetaData(Vector2 spritePivot, int pixelsPerUnit)
        {
            Dictionary metadatas = new Dictionary();
            for (int index = 0; index < Frames.Count; index++)
            {
                List layers = GetChunks();
                List cels = Frames[index].GetChunks();
                cels.Sort((ca, cb) => ca.LayerIndex.CompareTo(cb.LayerIndex));
                for (int i = 0; i < cels.Count; i++)
                {
                    int layerIndex = cels[i].LayerIndex;
                    LayerChunk layer = layers[layerIndex];
                    if (!layer.LayerName.StartsWith(MetaData.MetaDataChar)) //read only metadata layer
                        continue;
                    if (!metadatas.ContainsKey(layerIndex))
                        metadatas[layerIndex] = new MetaData(layer.LayerName);
                    var metadata = metadatas[layerIndex];
                    CelChunk cel = cels[i];
                    Vector2 center = Vector2.zero;
                    int pixelCount = 0;
                    for (int y = 0; y < cel.Height; ++y)
                    {
                        for (int x = 0; x < cel.Width; ++x)
                        {
                            int texX = cel.X + x;
                            int texY = -(cel.Y + y) + Header.Height - 1;
                            var col = cel.RawPixelData[x + y * cel.Width];
                            if (col.GetColor().a > 0.1f)
                            {
                                center += new Vector2(texX, texY);
                                pixelCount++;
                            }
                        }
                    }
                    if (pixelCount > 0)
                    {
                        center /= pixelCount;
                        var pivot = Vector2.Scale(spritePivot, new Vector2(Header.Width, Header.Height));
                        var posWorld = (center - pivot) / pixelsPerUnit + Vector2.one * 0.5f / pixelsPerUnit; //center pos in middle of pixels
                        metadata.Transforms.Add(index, posWorld);
                    }
                }
            }
            return metadatas.Values.ToArray();
        }
        public Texture2D GetTextureAtlas()
        {
            Texture2D[] frames = this.GetFrames();
            Texture2D atlas = Texture2DUtil.CreateTransparentTexture(Header.Width * frames.Length, Header.Height);
            List spriteRects = new List();
            int col = 0;
            int row = 0;
            foreach (Texture2D frame in frames)
            {
                Rect spriteRect = new Rect(col * Header.Width, atlas.height - ((row + 1) * Header.Height), Header.Width, Header.Height);
                atlas.SetPixels((int)spriteRect.x, (int)spriteRect.y, (int)spriteRect.width, (int)spriteRect.height, frame.GetPixels());
                atlas.Apply();
                spriteRects.Add(spriteRect);
                col++;
            }
            return atlas;
        }
    }
}