Code/Wasm/Interpret/LinearMemory.cs
using System;
using System.Collections.Generic;
using System.Collections.Immutable;

namespace WasmBox.Wasm.Interpret {
    /// <summary>
    /// Describes an instance of a linear memory specification.
    /// </summary>
    public sealed class LinearMemory {
        /// <summary>
        /// Creates a linear memory from the given specification.
        /// </summary>
        /// <param name="limits">The specification for this linear memory: a range in units of pages.</param>
        public LinearMemory(ResizableLimits limits) {
            this.Limits = limits;
            this.memory = new List<byte>();
            GrowToSize(limits.Initial);
        }

        private List<byte> memory;
        public Span<byte> Span => System.Runtime.InteropServices.CollectionsMarshal.AsSpan( memory );

        /// <summary>
        /// Gets the specification for this linear memory.
        /// </summary>
        /// <returns>The specification for this linear memory/</returns>
        public ResizableLimits Limits { get; private set; }

        /// <summary>
        /// Gets the size of the linear memory in units of pages.
        /// </summary>
        /// <returns>The size of the linear memory in units of pages.</returns>
        public uint Size => (uint)memory.Count / MemoryType.PageSize;
        
        public uint Length => (uint)memory.Count;

        /// <summary>
        /// Grows the memory to the given number of pages.
        /// Return the previous memory size in units of pages or -1 on failure.
        /// </summary>
        /// <param name="newSize">The new number of pages in the linear memory.</param>
        /// <returns>The previous memory size in units of pages or -1 on failure.</returns>
        private int GrowToSize( uint newSize ) {
            if ( Limits.HasMaximum && newSize > Limits.Maximum.Value ) {
                return -1;
            }

            int oldMemorySize = (int)Size;
            int newSizeInBytes = (int)(newSize * MemoryType.PageSize);

            // TODO: Optimize, might be quite slow
            while ( memory.Count < newSizeInBytes ) {
                memory.Add( 0 );
            }
            return oldMemorySize;
        }

        /// <summary>
        /// Grows linear memory by a given unsigned delta of pages.
        /// Return the previous memory size in units of pages or -1 on failure.
        /// </summary>
        /// <param name="numberOfPages">The number of pages to grow the linear memory by.</param>
        /// <returns>The previous memory size in units of pages or -1 on failure.</returns>
        public int Grow(uint numberOfPages) {
            return GrowToSize(Size + numberOfPages);
        }

        /// <summary>
        /// Accesses linear memory as a sequence of 8-bit signed integers.
        /// </summary>
        /// <returns>A view of this memory.</returns>
        public LinearMemoryAsInt8 Int8 {
            get { return new LinearMemoryAsInt8(memory); }
        }

        /// <summary>
        /// Accesses linear memory as a sequence of 16-bit signed integers.
        /// </summary>
        /// <returns>A view of this memory.</returns>
        public LinearMemoryAsInt16 Int16 {
            get { return new LinearMemoryAsInt16(memory); }
        }

        /// <summary>
        /// Accesses linear memory as a sequence of 32-bit signed integers.
        /// </summary>
        /// <returns>A view of this memory.</returns>
        public LinearMemoryAsInt32 Int32 {
            get { return new LinearMemoryAsInt32(memory); }
        }

        /// <summary>
        /// Accesses linear memory as a sequence of 64-bit signed integers.
        /// </summary>
        /// <returns>A view of this memory.</returns>
        public LinearMemoryAsInt64 Int64 {
            get { return new LinearMemoryAsInt64(memory); }
        }

        /// <summary>
        /// Accesses linear memory as a sequence of 32-bit floating-point numbers.
        /// </summary>
        /// <returns>A view of this memory.</returns>
        public LinearMemoryAsFloat32 Float32 {
            get { return new LinearMemoryAsFloat32(memory); }
        }

        /// <summary>
        /// Accesses linear memory as a sequence of 64-bit floating-point numbers.
        /// </summary>
        /// <returns>A view of this memory.</returns>
        public LinearMemoryAsFloat64 Float64 {
            get { return new LinearMemoryAsFloat64(memory); }
        }
        
        public byte this[uint offset] {
            get {
                CheckBounds(memory, offset, 1);
                return memory[(int)offset];
            }
            set {
                CheckBounds(memory, offset, 1);
                memory[(int)offset] = value;
            }
        }
        
        public Span<byte> this[Range range] {
            get => Span[range];
            set {
                value.CopyTo( Span[range] );
            }
        }

        internal static void CheckBounds( List<byte> memory, uint offset, uint length ) {
            if ( (ulong)memory.Count < (ulong)offset + (ulong)length ) {
                throw new TrapException(
                    $"Memory access out of bounds: cannot access {length} bytes at offset {offset} in memory with length {memory.Count}.",
                    TrapException.SpecMessages.OutOfBoundsMemoryAccess );
            }
        }
    }

    /// <summary>
    /// Accesses linear memory as a sequence of 8-bit signed integers.
    /// </summary>
    public struct LinearMemoryAsInt8 {
        internal LinearMemoryAsInt8(List<byte> memory) {
            this.mem = memory;
        }

        private List<byte> mem;

        /// <summary>
        /// Gets or sets a value in memory at a particular byte offset.
        /// </summary>
        /// <value>A value in memory.</value>
        public sbyte this[uint offset] {
            get {
                LinearMemory.CheckBounds(mem, offset, 1);
                return (sbyte)mem[(int)offset];
            }
            set {
                LinearMemory.CheckBounds(mem, offset, 1);
                mem[(int)offset] = (byte)value;
            }
        }
    }

    /// <summary>
    /// Accesses linear memory as a sequence of 16-bit signed integers.
    /// </summary>
    public struct LinearMemoryAsInt16 {
        internal LinearMemoryAsInt16(List<byte> memory) {
            this.mem = memory;
        }

        private List<byte> mem;

        /// <summary>
        /// Gets or sets a value in memory at a particular byte offset.
        /// </summary>
        /// <value>A value in memory.</value>
        public short this[uint offset] {
            get {
                LinearMemory.CheckBounds(mem, offset, 2);
                return (short)(
                    (uint)mem[(int)offset + 1] << 8 |
                    (uint)mem[(int)offset]);
            }
            set {
                LinearMemory.CheckBounds(mem, offset, 2);
                mem[(int)offset + 1] = (byte)(value >> 8);
                mem[(int)offset] = (byte)value;
            }
        }
    }

    /// <summary>
    /// Accesses linear memory as a sequence of 32-bit signed integers.
    /// </summary>
    public struct LinearMemoryAsInt32 {
        internal LinearMemoryAsInt32(List<byte> memory) {
            this.mem = memory;
        }

        private List<byte> mem;

        /// <summary>
        /// Gets or sets a value in memory at a particular byte offset.
        /// </summary>
        /// <value>A value in memory.</value>
        public int this[uint offset] {
            get {
                LinearMemory.CheckBounds(mem, offset, 4);
                return (int)mem[(int)offset + 3] << 24
                    | (int)mem[(int)offset + 2] << 16
                    | (int)mem[(int)offset + 1] << 8
                    | (int)mem[(int)offset];
            }
            set {
                LinearMemory.CheckBounds(mem, offset, 4);
                mem[(int)offset + 3] = (byte)(value >> 24);
                mem[(int)offset + 2] = (byte)(value >> 16);
                mem[(int)offset + 1] = (byte)(value >> 8);
                mem[(int)offset] = (byte)value;
            }
        }
    }

    /// <summary>
    /// Accesses linear memory as a sequence of 64-bit signed integers.
    /// </summary>
    public struct LinearMemoryAsInt64 {
        internal LinearMemoryAsInt64(List<byte> memory) {
            this.mem = memory;
        }

        private List<byte> mem;

        /// <summary>
        /// Gets or sets a value in memory at a particular byte offset.
        /// </summary>
        /// <value>A value in memory.</value>
        public long this[uint offset] {
            get {
                LinearMemory.CheckBounds(mem, offset, 8);
                return (long)mem[(int)offset + 7] << 56
                    | (long)mem[(int)offset + 6] << 48
                    | (long)mem[(int)offset + 5] << 40
                    | (long)mem[(int)offset + 4] << 32
                    | (long)mem[(int)offset + 3] << 24
                    | (long)mem[(int)offset + 2] << 16
                    | (long)mem[(int)offset + 1] << 8
                    | (long)mem[(int)offset];
            }
            set {
                LinearMemory.CheckBounds(mem, offset, 8);
                mem[(int)offset + 7] = (byte)(value >> 56);
                mem[(int)offset + 6] = (byte)(value >> 48);
                mem[(int)offset + 5] = (byte)(value >> 40);
                mem[(int)offset + 4] = (byte)(value >> 32);
                mem[(int)offset + 3] = (byte)(value >> 24);
                mem[(int)offset + 2] = (byte)(value >> 16);
                mem[(int)offset + 1] = (byte)(value >> 8);
                mem[(int)offset] = (byte)value;
            }
        }
    }

    /// <summary>
    /// Accesses linear memory as a sequence of 32-bit floating-point numbers.
    /// </summary>
    public struct LinearMemoryAsFloat32 {
        internal LinearMemoryAsFloat32(List<byte> memory) {
            this.mem = memory;
        }

        private List<byte> mem;

        /// <summary>
        /// Gets or sets a value in memory at a particular byte offset.
        /// </summary>
        /// <value>A value in memory.</value>
        public float this[uint offset] {
            get {
                return ValueHelpers.ReinterpretAsFloat32(new LinearMemoryAsInt32(mem)[offset]);
            }
            set {
                var uintView = new LinearMemoryAsInt32(mem);
                uintView[offset] = ValueHelpers.ReinterpretAsInt32(value);
            }
        }
    }

    /// <summary>
    /// Accesses linear memory as a sequence of 64-bit floating-point numbers.
    /// </summary>
    public struct LinearMemoryAsFloat64 {
        internal LinearMemoryAsFloat64(List<byte> memory) {
            this.mem = memory;
        }

        private List<byte> mem;

        /// <summary>
        /// Gets or sets a value in memory at a particular byte offset.
        /// </summary>
        /// <value>A value in memory.</value>
        public double this[uint offset] {
            get {
                return ValueHelpers.ReinterpretAsFloat64(new LinearMemoryAsInt64(mem)[offset]);
            }
            set {
                var uintView = new LinearMemoryAsInt64(mem);
                uintView[offset] = ValueHelpers.ReinterpretAsInt64(value);
            }
        }
    }
}