﻿using FSO.Files.Utils;
using System;
using System.Collections.Generic;
using System.IO;

namespace FSO.Files.Formats.IFF.Chunks
{
    public class TREE : IffChunk
    {
        public static TREE GenerateEmpty(BHAV bhav)
        {
            var result = new TREE();
            result.ChunkLabel = "";
            result.ChunkID = bhav.ChunkID;
            result.AddedByPatch = true;
            result.ChunkProcessed = true;
            result.RuntimeInfo = ChunkRuntimeState.Modified;
            result.ChunkType = "TREE";

            result.CorrectConnections(bhav);
            return result;
            /*
            var additionID = bhav.Instructions.Length;

            Func<byte, short> resolveTrueFalse = (byte pointer) =>
            {
                switch (pointer)
                {
                    case 253:
                        return -1;
                    case 255:
                        //generate false 
                    case 254:
                        //generate true
                }
                if (pointer == 255) return -1;
                else if (pointer == 2)
            };

            //make an entry for each instruction. positions and sizes don't matter - we have a runtime flag to indicate they are not valid
            for (int i=0; i<bhav.Instructions.Length; i++)
            {
                var inst = bhav.Instructions[i];
                var box = new TREEBox(result);
                box.InternalID = i;
                box.PosisionInvalid = true;
                box.Type = TREEBoxType.Primitive;
                box.TruePointer = 
            }
            */
        }

        public List<TREEBox> Entries = new List<TREEBox>();
        public int PrimitiveCount => Entries.FindLastIndex(x => x.Type == TREEBoxType.Primitive) + 1;

        //runtime
        public uint TreeVersion = 0;

        public override void Read(IffFile iff, Stream stream)
        {
            using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
            {
                var zero = io.ReadInt32();
                var version = io.ReadInt32();
                if (version > 1) throw new Exception("Unexpected TREE version: " + version);
                string magic = io.ReadCString(4); //HBGN
                if (magic != "EERT") throw new Exception("Magic number should be 'EERT', got " + magic);
                var entryCount = io.ReadInt32();
                Entries.Clear();
                for (int i=0; i<entryCount; i++)
                {
                    var box = new TREEBox(this);
                    box.Read(io, version);
                    box.InternalID = (short)i;
                    Entries.Add(box);
                }
            }
        }

        public override bool Write(IffFile iff, Stream stream)
        {
            using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
            {
                io.WriteInt32(0);
                io.WriteInt32(1);
                io.WriteCString("EERT", 4);
                io.WriteInt32(Entries.Count);
                foreach (var entry in Entries)
                {
                    entry.Write(io);
                }
            }
            return true;
        }

        public void ApplyPointerDelta(int delta, int after)
        {
            foreach (var box in Entries)
            {
                if (box.InternalID >= after) box.InternalID += (short)delta;
                if (box.TruePointer >= after) box.TruePointer += (short)delta;
                if (box.FalsePointer >= after) box.FalsePointer += (short)delta;
            }
        }

        public void CorrectConnections(BHAV bhav)
        {
            //make sure there are enough primitives for the bhav
            var realPrimCount = bhav.Instructions.Length;
            var treePrimCount = Entries.FindLastIndex(x => x.Type == TREEBoxType.Primitive) + 1;

            ApplyPointerDelta(realPrimCount-treePrimCount, treePrimCount);
            if (realPrimCount > treePrimCount)
            {
                //add new treeboxes
                for (int i=treePrimCount; i<realPrimCount; i++)
                {
                    var box = new TREEBox(this);
                    box.InternalID = (short)i;
                    box.PosisionInvalid = true;
                    box.Type = TREEBoxType.Primitive;
                    Entries.Insert(i, box);
                }
            }
            else if (treePrimCount > realPrimCount)
            {
                //remove treeboxes
                for (int i=treePrimCount; i>realPrimCount; i--)
                {
                    Entries.RemoveAt(i-1);
                }
            }

            //make sure connections for each of the primitives match the BHAV
            //if they don't, reconnect them or generate new boxes (true/false endpoints, maybe gotos in future)

            for (int i=0; i<realPrimCount; i++)
            {
                var prim = bhav.Instructions[i];
                var box = Entries[i];

                if (prim.TruePointer != GetTrueID(box.TruePointer))
                {
                    box.TruePointer = GetCorrectBox(prim.TruePointer);
                }
                if (prim.FalsePointer != GetTrueID((short)box.FalsePointer))
                {
                    box.FalsePointer = GetCorrectBox(prim.FalsePointer);
                }
            }
        }

        public void DeleteBox(TREEBox box)
        {
            //remove box. apply delta
            var id = box.InternalID;
            foreach (var box2 in Entries)
            {
                if (box2.TruePointer == id) box2.TruePointer = -1;
                if (box2.FalsePointer == id) box2.FalsePointer = -1;
            }
            Entries.RemoveAt(id);
            ApplyPointerDelta(-1, id);
        }

        public void InsertPrimitiveBox(TREEBox box)
        {
            var primEnd = PrimitiveCount;
            ApplyPointerDelta(1, primEnd);

            box.InternalID = (short)primEnd;
            Entries.Insert(primEnd, box);
        }

        public void InsertRemovedBox(TREEBox box)
        {
            var oldIndex = box.InternalID;
            ApplyPointerDelta(1, oldIndex);
            Entries.Insert(oldIndex, box);
        }

        public TREEBox MakeNewPrimitiveBox(TREEBoxType type)
        {
            var primEnd = PrimitiveCount;
            ApplyPointerDelta(1, primEnd);
            //find end of primitives and add box there. apply delta
            var box = new TREEBox(this);
            box.InternalID = (short)primEnd;
            box.PosisionInvalid = true;
            box.Type = type;
            Entries.Insert(primEnd, box);
            return box;
        }

        public TREEBox MakeNewSpecialBox(TREEBoxType type)
        {
            //add box at end. no delta needs to be applied.
            var box = new TREEBox(this);
            box.InternalID = (short)Entries.Count;
            box.PosisionInvalid = true;
            box.Type = type;
            Entries.Add(box);
            return box;
        }

        private short GetCorrectBox(byte realID)
        {
            switch (realID)
            {
                case 255:
                    //create false box
                    var f = MakeNewSpecialBox(TREEBoxType.False);
                    return f.InternalID;
                case 254:
                    //create true box
                    var t = MakeNewSpecialBox(TREEBoxType.True);
                    return t.InternalID;
                case 253:
                    return -1;
                default:
                    return realID;
            }
        }

        public byte GetTrueID(short boxID)
        {
            return GetBox(boxID)?.TrueID ?? 253;
        }

        public TREEBox GetBox(short pointer)
        {
            if (pointer < 0 || pointer >= Entries.Count) return null;
            return Entries[pointer];
        }
    }

    public class TREEBox
    {
        //runtime
        public short InternalID = -1;
        public bool PosisionInvalid; //forces a regeneration of position using the default tree algorithm
        public TREE Parent;
        public byte TrueID
        {
            get
            {
                switch (Type)
                {
                    case TREEBoxType.Primitive:
                        return (byte)InternalID;
                    case TREEBoxType.Goto:
                        return LabelTrueID(new HashSet<short>());
                    case TREEBoxType.Label:
                        return 253; //arrows cannot point to a label
                    case TREEBoxType.True:
                        return 254;
                    case TREEBoxType.False:
                        return 255;
                }
                return 253;
            }
        }

        public byte LabelTrueID(HashSet<short> visited)
        {
            if (Type != TREEBoxType.Goto) return TrueID;
            if (visited.Contains(InternalID)) return 253; //error
            visited.Add(InternalID);
            return Parent?.GetBox(Parent.GetBox(TruePointer)?.TruePointer ?? -1)?.LabelTrueID(visited) ?? 253;
        }

        //data
        public TREEBoxType Type;
        public ushort Unknown;
        public short Width;
        public short Height;
        public short X;
        public short Y;
        public short CommentSize = 0x10;
        public short TruePointer = -1;
        public short Special; //0 or -1... unknown.
        public int FalsePointer = -1;
        public string Comment = "";
        public int TrailingZero = 0;

        public TREEBox(TREE parent)
        {
            Parent = parent;
        }

        public void Read(IoBuffer io, int version)
        {
            Type = (TREEBoxType)io.ReadUInt16();
            Unknown = io.ReadUInt16();
            Width = io.ReadInt16();
            Height = io.ReadInt16();
            X = io.ReadInt16();
            Y = io.ReadInt16();
            CommentSize = io.ReadInt16();
            TruePointer = io.ReadInt16();
            Special = io.ReadInt16();
            FalsePointer = io.ReadInt32();
            Comment = io.ReadNullTerminatedString();
            if (Comment.Length % 2 == 0) io.ReadByte(); //padding to 2 byte align
            if (version > 0) TrailingZero = io.ReadInt32();

            if (!Enum.IsDefined(typeof(TREEBoxType), Type)) throw new Exception("Unexpected TREE box type: " + Type.ToString());
            if (Special < -1 || Special > 0) throw new Exception("Unexpected TREE special: " + Special);
            if (Unknown != 0) throw new Exception("Unexpected Unknown: " + Unknown);
            if (TrailingZero != 0) Console.WriteLine("Unexpected TrailingZero: " + TrailingZero);
        }

        public void Write(IoWriter io)
        {
            io.WriteUInt16((ushort)Type);
            io.WriteUInt16(Unknown);
            io.WriteInt16(Width);
            io.WriteInt16(Height);
            io.WriteInt16(X);
            io.WriteInt16(Y);
            io.WriteInt16(CommentSize);
            io.WriteInt16(TruePointer);
            io.WriteInt16(Special);
            io.WriteInt32(FalsePointer);
            io.WriteCString(Comment);
            if (Comment.Length % 2 == 0) io.WriteByte(0xCD); //padding to 2 byte align
            io.WriteInt32(TrailingZero);
        }

        public override string ToString()
        {
            return Type.ToString() + " (" + TruePointer + ((FalsePointer == -1) ? "" : ("/"+FalsePointer)) + "): " + Comment;
        }
    }

    public enum TREEBoxType : ushort
    {
        Primitive = 0,
        True = 1,
        False = 2,
        Comment = 3,
        Label = 4,
        Goto = 5 //no comment size, roughly primitive sized (180, 48), pointer goes to Label
    }
}
