// UndoRedoSurfaceBuffer.cs
//  Copyright (c) 2015 jeffrey.io
using System;
using System.Collections.Generic;

namespace io.jeffrey.pixel {
    public class UndoRedoBuffer {
        public readonly PixelSurface Surface;
        public readonly bool ReadOnly;
        public readonly int Width;
        public readonly int Height;
        private readonly UndoEncoding Encoding;
        private readonly int RecordSize;
        private readonly int[] Buffer;
        private int Position;

        public UndoRedoBuffer(int width, int height) {
            this.Surface = null;
            this.ReadOnly = false;
            this.Width = width;
            this.Height = height;
            if (width * height < Int32.MaxValue / 4) {
                this.Encoding = UndoEncoding.Packed;
                this.RecordSize = 3;
            } else {
                this.Encoding = UndoEncoding.Unpacked;
                this.RecordSize = 4;
            }
            this.Buffer = new int[RecordSize * width * height];
            this.Position = 0;
        }

        private UndoRedoBuffer(PixelSurface surface, UndoRedoBuffer parent) {
            this.Surface = surface;
            this.ReadOnly = true;
            this.Width = parent.Width;
            this.Height = parent.Height;
            this.Encoding = parent.Encoding;
            this.RecordSize = parent.RecordSize;
            this.Buffer = new int[parent.Position];
            this.Position = parent.Position;
            for (int k = 0; k < parent.Position; k++) {
                Buffer[k] = parent.Buffer[k];
            }
        }

        public int Size {
            get {
                return Buffer.Length;
            }
        }

        public bool HasChanges {
            get {
                return Position > 0;
            }
        }

        public UndoRedoBuffer Snap(PixelSurface surface) {
            return new UndoRedoBuffer(surface, this);
        }

        public void Reset() {
            this.Position = 0;
        }

        public void Walk(Action<int,int,int,int> delta) {
            for (int at = 0; at < Position; at+=RecordSize) {
                if (this.Encoding == UndoEncoding.Packed) {
                    int v = Buffer[at];
                    int x = v % Width;
                    delta(x, (v - x) / Width, Buffer[at+1], Buffer[at+2]);
                } else {
                    delta(Buffer[at], Buffer[at+1], Buffer[at+2], Buffer[at+3]);
                }
            }
        }

        public void Write(int x, int y, int before, int after) {
            if (this.Encoding == UndoEncoding.Packed) {
                Buffer[Position] = y * Width + x;
                Buffer[Position+1] = before;
                Buffer[Position+2] = after;
            } else {
                Buffer[Position] = x;
                Buffer[Position+1] = y;
                Buffer[Position+2] = before;
                Buffer[Position+3] = after;
            }
            Position += RecordSize;
        }
    }

    public class UndoRedoSurfaceBuffer : PixelSurface {
        private readonly int MaximumStackSize;
        private PixelSurface CurrentSurface;
        private readonly UndoRedoBuffer Buffer;
        private readonly Stack<UndoRedoBuffer> UndoStack = new Stack<UndoRedoBuffer>();
        private readonly Stack<UndoRedoBuffer> RedoStack = new Stack<UndoRedoBuffer>();
        private readonly Stack<UndoRedoBuffer> Temp = new Stack<UndoRedoBuffer>();
        private bool Allowed = false;

        public UndoRedoSurfaceBuffer(int width, int height) {
            this.Buffer = new UndoRedoBuffer(width, height);
            this.MaximumStackSize = (width * height * 4) * 5;
        }

        public void BeginRecord(PixelSurface Surface) {
            if (Allowed)
                EndRecord();
            this.CurrentSurface = Surface;
            Allowed = true;
        }

        public void EndRecord() {
            Allowed = false;
            if (Buffer.HasChanges) {
                UndoStack.Push(Buffer.Snap(CurrentSurface));
                Balance();
                Buffer.Reset();
            }
            if (RedoAvailable) {
                RedoStack.Clear();
            }
        }

        private void Balance(Stack<UndoRedoBuffer> stack) {
            Temp.Clear();
            int Score = 0;
            while(stack.Count > 0) {
                UndoRedoBuffer test = stack.Pop();
                Score += test.Size;
                if (Score < MaximumStackSize) {
                    Temp.Push(test);
                }
            }
            while (Temp.Count > 0) {
                stack.Push(Temp.Pop());
            }
        }

        private void Balance() {
            Balance(UndoStack);
            Balance(RedoStack);
        }

        public bool UndoAvailable {
            get {
                return UndoStack.Count > 0;
            }
        }

        public bool RedoAvailable {
            get {
                return RedoStack.Count > 0;
            }
        }

        public void Undo() {
            if (UndoStack.Count == 0)
                return;

            UndoRedoBuffer undo = UndoStack.Pop();
            RedoStack.Push(undo);
            Balance();

            undo.Walk((x,y,before,after) => {
                undo.Surface.Put(x,y,before);
            });
        }

        public void Redo() {
            if (RedoStack.Count == 0)
                return;

            UndoRedoBuffer redo = RedoStack.Pop();
            UndoStack.Push(redo);
            Balance();

            redo.Walk((x,y,before,after) => {
                redo.Surface.Put(x,y,after);
            });
        }

        private void Record(int x, int y, int before, int after) {
            Buffer.Write(x, y, before, after);
        }

        public PixelSurface Put(int x, int y, int arba) {
            if (!Allowed) {
                throw new InvalidOperationException();
            }
            SurfacePoint before = CurrentSurface.Get(x, y);
            if (before.Valid) {
                if (before.Color != arba) {
                    Record(x, y, before.Color, arba);
                    CurrentSurface.Put(x, y, arba);
                }
                return this;
            } else {
                return this;
            }
        }

        public SurfacePoint Get(int x, int y) {
            return CurrentSurface.Get(x, y);
        }

        public void Draw() {
            throw new NotSupportedException();
        }

        public PixelSurface Kinkolate(Func<int, int, KinkolateAction> operation, Action<int, int, int, int> delta) {
            return CurrentSurface.Kinkolate(operation, (x,y,before,after) => {
                if (before != after) {
                    Record(x, y, before, after);
                }
                // inspect it
                if (delta != null) {
                    delta(x, y, before, after);
                }
            });
        }

        public void Release() {
            throw new NotSupportedException();

        }

        public PixelSurfaceType Type {
            get {
                return CurrentSurface.Type;
            }
        }

        public int Width {
            get {
                return CurrentSurface.Width;
            }
        }

        public int Height {
            get {
                return CurrentSurface.Height;
            }
        }

        public int TranslationX {
            get {
                return CurrentSurface.TranslationX;
            }
            set {
                CurrentSurface.TranslationX = value;
            }
        }

        public int TranslationY {
            get {
                return CurrentSurface.TranslationY;
            }
            set {
                CurrentSurface.TranslationY = value;
            }
        }

        public long LastChange {
            get {
                return CurrentSurface.LastChange;
            }
        }
    }
}

