#!/usr/bin/env bash
#
# Generate concrete implementations of LatestMap.
#
# e.g.
#     $ generate_latest_map ./report/out.go string NodeControlData ...
#
# Depends on:
# - gofmt

function generate_header() {
    local out_file="${1}"
    local cmd="${2}"

    cat <<EOF >"${out_file}"
    // Generated file, do not edit.
    // To regenerate, run ${cmd}

    package report

    import (
		"bytes"
        "fmt"
		"sort"
        "time"

        "github.com/ugorji/go/codec"
    )
EOF
}

function generate_latest_map() {
    local out_file="$1"
    local data_type="$2"
    local uppercase_data_type="${data_type^}"
    local lowercase_data_type="${data_type,}"
    local entry_type="${lowercase_data_type}LatestEntry"
    local latest_map_type="${uppercase_data_type}LatestMap"
    local make_function="Make${latest_map_type}"

    # shellcheck disable=SC2016
    local json_timestamp='`json:"timestamp"`'
    # shellcheck disable=SC2016
    local json_value='`json:"value"`'

    cat <<EOF >>"${out_file}"
    type ${entry_type} struct {
        key       string
        Timestamp time.Time    ${json_timestamp}
        Value     ${data_type} ${json_value}
        dummySelfer
    }

    // String returns the StringLatestEntry's string representation.
    func (e *${entry_type}) String() string {
        return fmt.Sprintf("%v (%s)", e.Value, e.Timestamp.String())
    }

    // Equal returns true if the supplied StringLatestEntry is equal to this one.
    func (e *${entry_type}) Equal(e2 *${entry_type}) bool {
        return e.Timestamp.Equal(e2.Timestamp) && e.Value == e2.Value
    }

    // ${latest_map_type} holds latest ${data_type} instances, as a slice sorted by key.
    type ${latest_map_type} struct { entries []${entry_type} }

    // ${make_function} makes an empty ${latest_map_type}.
    func ${make_function}() ${latest_map_type} {
        return ${latest_map_type}{}
    }

    // Size returns the number of elements.
    func (m ${latest_map_type}) Size() int {
        return len(m.entries)
    }

    // Merge produces a fresh ${latest_map_type} containing the keys from both inputs.
    // When both inputs contain the same key, the newer value is used.
    func (m ${latest_map_type}) Merge(n ${latest_map_type}) ${latest_map_type} {
        switch {
        case m.entries == nil:
            return n
        case n.entries == nil:
            return m
        }
        out := make([]${entry_type}, 0, len(m.entries)+len(n.entries))

        i, j := 0, 0
        for i < len(m.entries) {
            switch {
            case j >= len(n.entries) || m.entries[i].key < n.entries[j].key:
                out = append(out, m.entries[i])
                i++
            case m.entries[i].key == n.entries[j].key:
                if m.entries[i].Timestamp.Before(n.entries[j].Timestamp) {
                    out = append(out, n.entries[j])
                } else {
                    out = append(out, m.entries[i])
                }
                i++
                j++
            default:
                out = append(out, n.entries[j])
                j++
            }
        }
        out = append(out, n.entries[j:]...)
        return ${latest_map_type}{out}
    }

    // Lookup the value for the given key.
    func (m ${latest_map_type}) Lookup(key string) (${data_type}, bool) {
        v, _, ok := m.LookupEntry(key)
        if !ok {
            var zero ${data_type}
            return zero, false
        }
        return v, true
    }

    // LookupEntry returns the raw entry for the given key.
    func (m ${latest_map_type}) LookupEntry(key string) (${data_type}, time.Time, bool) {
        i := sort.Search(len(m.entries), func(i int) bool {
            return m.entries[i].key >= key
        })
        if i < len(m.entries) && m.entries[i].key == key {
            return m.entries[i].Value, m.entries[i].Timestamp, true
        }
        var zero ${data_type}
        return zero, time.Time{}, false
    }

    // locate the position where key should go, and make room for it if not there already
    func (m *${latest_map_type}) locate(key string) int {
        i := sort.Search(len(m.entries), func(i int) bool {
            return m.entries[i].key >= key
        })
        // i is now the position where key should go, either at the end or in the middle
        if i == len(m.entries) || m.entries[i].key != key {
            m.entries = append(m.entries, ${entry_type}{})
            copy(m.entries[i+1:], m.entries[i:])
        }
        return i
    }

    // Set the value for the given key.
    func (m ${latest_map_type}) Set(key string, timestamp time.Time, value ${data_type}) ${latest_map_type} {
        i := sort.Search(len(m.entries), func(i int) bool {
            return m.entries[i].key >= key
        })
        // i is now the position where key should go, either at the end or in the middle
        oldEntries := m.entries
        if i == len(m.entries) {
            m.entries = make([]${entry_type}, len(oldEntries)+1)
            copy(m.entries, oldEntries)
        } else if m.entries[i].key == key {
            m.entries = make([]${entry_type}, len(oldEntries))
            copy(m.entries, oldEntries)
        } else {
            m.entries = make([]${entry_type}, len(oldEntries)+1)
            copy(m.entries, oldEntries[:i])
            copy(m.entries[i+1:], oldEntries[i:])
        }
        m.entries[i] = ${entry_type}{key: key, Timestamp: timestamp, Value: value}
        return m
    }

    // ForEach executes fn on each key value pair in the map.
    func (m ${latest_map_type}) ForEach(fn func(k string, timestamp time.Time, v ${data_type})) {
        for _, value := range m.entries {
            fn(value.key, value.Timestamp, value.Value)
        }
    }

    // String returns the ${latest_map_type}'s string representation.
    func (m ${latest_map_type}) String() string {
        buf := bytes.NewBufferString("{")
        for _, val := range m.entries {
            fmt.Fprintf(buf, "%s: %s,\n", val.key, val)
        }
        fmt.Fprintf(buf, "}")
        return buf.String()
    }

    // DeepEqual tests equality with other ${latest_map_type}.
    func (m ${latest_map_type}) DeepEqual(n ${latest_map_type}) bool {
        if m.Size() != n.Size() {
            return false
        }
        for i := range m.entries {
            if m.entries[i].key != n.entries[i].key || !m.entries[i].Equal(&n.entries[i]) {
                return false
            }
        }
        return true
    }

    // CodecEncodeSelf implements codec.Selfer.
    // Duplicates the output for a built-in map without generating an
    // intermediate copy of the data structure, to save time.  Note this
    // means we are using undocumented, internal APIs, which could break
    // in the future.  See https://github.com/weaveworks/scope/pull/1709
    // for more information.
    func (m *${latest_map_type}) CodecEncodeSelf(encoder *codec.Encoder) {
        z, r := codec.GenHelperEncoder(encoder)
        if m.entries == nil {
            r.EncodeNil()
            return
        }
        r.EncodeMapStart(m.Size())
        for _, val := range m.entries {
            z.EncSendContainerState(containerMapKey)
            r.EncodeString(cUTF8, val.key)
            z.EncSendContainerState(containerMapValue)
            val.CodecEncodeSelf(encoder)
        }
        z.EncSendContainerState(containerMapEnd)
    }

    // CodecDecodeSelf implements codec.Selfer.
    // Decodes the input as for a built-in map, without creating an
    // intermediate copy of the data structure to save time. Uses
    // undocumented, internal APIs as for CodecEncodeSelf.
    func (m *${latest_map_type}) CodecDecodeSelf(decoder *codec.Decoder) {
        m.entries = nil
        z, r := codec.GenHelperDecoder(decoder)
        if r.TryDecodeAsNil() {
            return
        }

        length := r.ReadMapStart()
        if length > 0 {
            m.entries = make([]${entry_type}, 0, length)
        }
        for i := 0; length < 0 || i < length; i++ {
            if length < 0 && r.CheckBreak() {
                break
            }
            z.DecSendContainerState(containerMapKey)
            var key string
            if !r.TryDecodeAsNil() {
                key = r.DecodeString()
            }
            i := m.locate(key)
            m.entries[i].key = key
            z.DecSendContainerState(containerMapValue)
            if !r.TryDecodeAsNil() {
                m.entries[i].CodecDecodeSelf(decoder)
            }
        }
        z.DecSendContainerState(containerMapEnd)
    }

    // MarshalJSON shouldn't be used, use CodecEncodeSelf instead.
    func (${latest_map_type}) MarshalJSON() ([]byte, error) {
        panic("MarshalJSON shouldn't be used, use CodecEncodeSelf instead")
    }

    // UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead.
    func (*${latest_map_type}) UnmarshalJSON(b []byte) error {
        panic("UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead")
    }
EOF
}

if [ -z "${1}" ]; then
    echo "No output file given"
    exit 1
fi

out="${1}"
outtmp="${out}.tmp"

generate_header "${outtmp}" "${0} ${*}"
shift
for t in "${@}"; do
    generate_latest_map "${outtmp}" "${t}"
done

gofmt -s -w "${outtmp}"
mv "${outtmp}" "${out}"
