#!/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.Format(time.RFC3339))
    }

    // 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} []${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)
    }

    // Merge produces a ${latest_map_type} containing the keys from both inputs.
    // When both inputs contain the same key, the newer value is used.
    // Tries to return one of its inputs, if that already holds the correct result.
    func (m ${latest_map_type}) Merge(n ${latest_map_type}) ${latest_map_type} {
        switch {
        case len(m) == 0:
            return n
        case len(n) == 0:
            return m
        }
        if len(n) > len(m) {
            m, n = n, m //swap so m is always at least as long as n
        } else if len(n) == len(m) && m[0].Timestamp.Before(n[0].Timestamp) {
            // Optimise common case where we merge two nodes with the same contents
            // sampled at different times.
            m, n = n, m // swap equal-length arrays so first element of m is newer
        }

        i, j := 0, 0
    loop:
        for i < len(m) {
            switch {
            case j >= len(n):
                return m
            case m[i].key == n[j].key:
                if m[i].Timestamp.Before(n[j].Timestamp) {
                    break loop
                }
                i++
                j++
            case m[i].key < n[j].key:
                i++
            default:
                break loop
            }
        }
        if i >= len(m) && j >= len(n) {
            return m
        }

        out := make([]${entry_type}, i, len(m))
        copy(out, m[:i])

        for i < len(m) {
            switch {
            case j >= len(n):
                out = append(out, m[i:]...)
                return out
            case m[i].key == n[j].key:
                if m[i].Timestamp.Before(n[j].Timestamp) {
                    out = append(out, n[j])
                } else {
                    out = append(out, m[i])
                }
                i++
                j++
            case m[i].key < n[j].key:
                out = append(out, m[i])
                i++
            default:
                out = append(out, n[j])
                j++
            }
        }
        out = append(out, n[j:]...)
        return 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), func(i int) bool {
            return m[i].key >= key
        })
        if i < len(m) && m[i].key == key {
            return m[i].Value, m[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), func(i int) bool {
            return (*m)[i].key >= key
        })
        // i is now the position where key should go, either at the end or in the middle
        if i == len(*m) || (*m)[i].key != key {
            *m = append(*m, ${entry_type}{})
            copy((*m)[i+1:], (*m)[i:])
            (*m)[i] = ${entry_type}{}
        }
        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), func(i int) bool {
            return m[i].key >= key
        })
        // i is now the position where key should go, either at the end or in the middle
        oldEntries := m
        if i == len(m) {
            m = make([]${entry_type}, len(oldEntries)+1)
            copy(m, oldEntries)
        } else if m[i].key == key {
            m = make([]${entry_type}, len(oldEntries))
            copy(m, oldEntries)
        } else {
            m = make([]${entry_type}, len(oldEntries)+1)
            copy(m, oldEntries[:i])
            copy(m[i+1:], oldEntries[i:])
        }
        m[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 {
            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 {
            fmt.Fprintf(buf, "%s: %s,\n", val.key, val.String())
        }
        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 {
            if m[i].key != n[i].key || !m[i].Equal(&n[i]) {
                return false
            }
        }
        return true
    }

    // EqualIgnoringTimestamps returns true if all keys and values are the same.
    func (m ${latest_map_type}) EqualIgnoringTimestamps(n ${latest_map_type}) bool {
        if m.Size() != n.Size() {
            return false
        }
        for i := range m {
            if m[i].key != n[i].key || m[i].Value != n[i].Value {
                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 == nil {
            r.EncodeNil()
            return
        }
        r.EncodeMapStart(m.Size())
        for _, val := range m {
            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 = nil
        z, r := codec.GenHelperDecoder(decoder)
        if r.TryDecodeAsNil() {
            return
        }

        length := r.ReadMapStart()
        if length > 0 {
            *m = 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 = lookupCommonKey(r.DecodeStringAsBytes())
            }
            i := m.locate(key)
            (*m)[i].key = key
            z.DecSendContainerState(containerMapValue)
            if !r.TryDecodeAsNil() {
                (*m)[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}"
