// Copyright 2018 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import std

namespace flatbuffers

class handle:
    buf_:string
    pos_:int

// More strongly typed than a naked int, at no cost.
struct offset:
    o:int

enum sizeof:
    sz_8 = 1
    sz_16 = 2
    sz_32 = 4
    sz_64 = 8
    sz_voffset = 2
    sz_uoffset = 4
    sz_soffset = 4
    sz_metadata_fields = 2

class builder:
    buf = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    current_vtable:[int] = []
    head = 0
    minalign = 1
    object_end = 0
    vtables:[int] = []
    nested = false
    finished = false

    // Optionally call this right after creating the builder for a larger initial buffer.
    def Initial(initial_size:int):
        buf = "\x00".repeat_string(initial_size)

    def Start():
        // Get the start of useful data in the underlying byte buffer.
        return buf.length - head

    def Offset():
        // Offset relative to the end of the buffer.
        return offset { head }

    // Returns a copy of the part of the buffer containing only the finished FlatBuffer
    def SizedCopy():
        assert finished
        return buf.substring(Start(), -1)

    def StartNesting():
        assert not nested
        nested = true

    def EndNesting():
        assert nested
        nested = false

    def StartObject(numfields):
        StartNesting()
        current_vtable = map(numfields): 0
        object_end = head
        minalign = 1

    def EndObject():
        EndNesting()
        // Prepend a zero scalar to the object. Later in this function we'll
        // write an offset here that points to the object's vtable:
        PrependInt32(0)
        let object_offset = head
        // Write out new vtable speculatively.
        let vtable_size = (current_vtable.length + sz_metadata_fields) * sz_voffset
        while current_vtable.length:
            let o = current_vtable.pop()
            PrependVOffsetT(if o: object_offset - o else: 0)
        // The two metadata fields are written last.
        // First, store the object bytesize:
        PrependVOffsetT(object_offset - object_end)
        // Second, store the vtable bytesize:
        PrependVOffsetT(vtable_size)
        // Search backwards through existing vtables, because similar vtables
        // are likely to have been recently appended. See
        // BenchmarkVtableDeduplication for a case in which this heuristic
        // saves about 30% of the time used in writing objects with duplicate
        // tables.
        def find_existing_table():
            reverse(vtables) vt2_offset:
                // Find the other vtable:
                let vt2_start = buf.length - vt2_offset
                let vt2_len = buf.read_int16_le(vt2_start)
                // Compare the other vtable to the one under consideration.
                // If they are equal, return the offset:
                if vtable_size == vt2_len and
                    not compare_substring(buf, Start(), buf, vt2_start, vtable_size):
                        return vt2_offset
            return 0
        let existing_vtable = find_existing_table()
        if existing_vtable:
            // Found a duplicate vtable, remove the one we wrote.
            head = object_offset
            // Write the offset to the found vtable in the
            // already-allocated offset at the beginning of this object:
            buf.write_int32_le(Start(), existing_vtable - object_offset)
        else:
            // Did not find a vtable, so keep the one we wrote.
            // Next, write the offset to the new vtable in the
            // already-allocated offset at the beginning of this object:
            buf.write_int32_le(buf.length - object_offset, head - object_offset)
            // Finally, store this vtable in memory for future
            // deduplication:
            vtables.push(head)
        return offset { object_offset }

    def Pad(n):
        for(n):
            buf, head = buf.write_int8_le_back(head, 0)

    def Prep(size, additional_bytes):
        // Track the biggest thing we've ever aligned to.
        if size > minalign:
            minalign = size
        // Find the amount of alignment needed such that `size` is properly
        // aligned after `additionalBytes`:
        let align_size = ((~(head + additional_bytes)) + 1) & (size - 1)
        Pad(align_size)

    def PrependUOffsetTRelative(off:offset):
        // Prepends an unsigned offset into vector data, relative to where it will be written.
        Prep(sz_uoffset, 0)
        assert off.o <= head
        PlaceUOffsetT(head - off.o + sz_uoffset)

    def StartVector(elem_size, num_elems, alignment):
        // Initializes bookkeeping for writing a new vector.
        StartNesting()
        Prep(sz_32, elem_size * num_elems)
        Prep(alignment, elem_size * num_elems)  // In case alignment > int.
        return Offset()

    def EndVector(vector_num_elems):
        EndNesting()
        // we already made space for this, so write without PrependUint32
        PlaceUOffsetT(vector_num_elems)
        return Offset()

    def CreateString(s:string):
        // writes a null-terminated byte string.
        StartNesting()
        Prep(sz_32, s.length + 1)
        buf, head = buf.write_substring_back(head, s, true)
        return EndVector(s.length)

    def CreateByteVector(s:string):
        // writes a non-null-terminated byte string.
        StartNesting()
        Prep(sz_32, s.length)
        buf, head = buf.write_substring_back(head, s, false)
        return EndVector(s.length)

    def Slot(slotnum):
        assert nested
        while current_vtable.length <= slotnum: current_vtable.push(0)
        current_vtable[slotnum] = head

    def __Finish(root_table:offset, size_prefix:int, file_identifier:string?):
        // Finish finalizes a buffer, pointing to the given root_table
        assert not finished
        assert not nested
        var prep_size = sz_32
        if file_identifier:
            prep_size += sz_32
        if size_prefix:
            prep_size += sz_32
        Prep(minalign, prep_size)
        if file_identifier:
            assert file_identifier.length == 4
            buf, head = buf.write_substring_back(head, file_identifier, false)
        PrependUOffsetTRelative(root_table)
        if size_prefix:
            PrependInt32(head)
        finished = true
        return Start()

    def Finish(root_table:offset, file_identifier:string? = nil):
        return __Finish(root_table, false, file_identifier)

    def FinishSizePrefixed(root_table:offset, file_identifier:string? = nil):
        return __Finish(root_table, true, file_identifier)

    def PrependBool(x):
        buf, head = buf.write_int8_le_back(head, x)

    def PrependByte(x):
        buf, head = buf.write_int8_le_back(head, x)

    def PrependUint8(x):
        buf, head = buf.write_int8_le_back(head, x)

    def PrependUint16(x):
        Prep(sz_16, 0)
        buf, head = buf.write_int16_le_back(head, x)

    def PrependUint32(x):
        Prep(sz_32, 0)
        buf, head = buf.write_int32_le_back(head, x)

    def PrependUint64(x):
        Prep(sz_64, 0)
        buf, head = buf.write_int64_le_back(head, x)

    def PrependInt8(x):
        buf, head = buf.write_int8_le_back(head, x)

    def PrependInt16(x):
        Prep(sz_16, 0)
        buf, head = buf.write_int16_le_back(head, x)

    def PrependInt32(x):
        Prep(sz_32, 0)
        buf, head = buf.write_int32_le_back(head, x)

    def PrependInt64(x):
        Prep(sz_64, 0)
        buf, head = buf.write_int64_le_back(head, x)

    def PrependFloat32(x):
        Prep(sz_32, 0)
        buf, head = buf.write_float32_le_back(head, x)

    def PrependFloat64(x):
        Prep(sz_64, 0)
        buf, head = buf.write_float64_le_back(head, x)

    def PrependVOffsetT(x):
        Prep(sz_voffset, 0)
        buf, head = buf.write_int16_le_back(head, x)

    def PlaceVOffsetT(x):
        buf, head = buf.write_int16_le_back(head, x)

    def PlaceSOffsetT(x):
        buf, head = buf.write_int32_le_back(head, x)

    def PlaceUOffsetT(x):
        buf, head = buf.write_int32_le_back(head, x)

    def PrependSlot(o:int, x, d, f):
        if x != d:
            f(x)
            Slot(o)

    def PrependSlot(o:int, x, f):
        f(x)
        Slot(o)

    def PrependBoolSlot(o, x, d): PrependSlot(o, x, d): PrependBool(_)
    def PrependByteSlot(o, x, d): PrependSlot(o, x, d): PrependByte(_)
    def PrependUint8Slot(o, x, d): PrependSlot(o, x, d): PrependUint8(_)
    def PrependUint16Slot(o, x, d): PrependSlot(o, x, d): PrependUint16(_)
    def PrependUint32Slot(o, x, d): PrependSlot(o, x, d): PrependUint32(_)
    def PrependUint64Slot(o, x, d): PrependSlot(o, x, d): PrependUint64(_)
    def PrependInt8Slot(o, x, d): PrependSlot(o, x, d): PrependInt8(_)
    def PrependInt16Slot(o, x, d): PrependSlot(o, x, d): PrependInt16(_)
    def PrependInt32Slot(o, x, d): PrependSlot(o, x, d): PrependInt32(_)
    def PrependInt64Slot(o, x, d): PrependSlot(o, x, d): PrependInt64(_)
    def PrependFloat32Slot(o, x, d): PrependSlot(o, x, d): PrependFloat32(_)
    def PrependFloat64Slot(o, x, d): PrependSlot(o, x, d): PrependFloat64(_)

    def PrependBoolSlot(o, x): PrependSlot(o, x): PrependBool(_)
    def PrependByteSlot(o, x): PrependSlot(o, x): PrependByte(_)
    def PrependUint8Slot(o, x): PrependSlot(o, x): PrependUint8(_)
    def PrependUint16Slot(o, x): PrependSlot(o, x): PrependUint16(_)
    def PrependUint32Slot(o, x): PrependSlot(o, x): PrependUint32(_)
    def PrependUint64Slot(o, x): PrependSlot(o, x): PrependUint64(_)
    def PrependInt8Slot(o, x): PrependSlot(o, x): PrependInt8(_)
    def PrependInt16Slot(o, x): PrependSlot(o, x): PrependInt16(_)
    def PrependInt32Slot(o, x): PrependSlot(o, x): PrependInt32(_)
    def PrependInt64Slot(o, x): PrependSlot(o, x): PrependInt64(_)
    def PrependFloat32Slot(o, x): PrependSlot(o, x): PrependFloat32(_)
    def PrependFloat64Slot(o, x): PrependSlot(o, x): PrependFloat64(_)

    def PrependUOffsetTRelativeSlot(o:int, x:offset):
        if x.o:
            PrependUOffsetTRelative(x)
            Slot(o)

    def PrependStructSlot(v:int, x:offset):
        if x.o:
            // Structs are always stored inline, so need to be created right
            // where they are used. You'll get this error if you created it
            // elsewhere.
            assert x.o == head
            Slot(v)

def has_identifier(buf:string, file_identifier:string):
    assert file_identifier.length == 4
    return buf.length >= 8 and buf.substring(4, 4) == file_identifier


