#!/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 (
        "fmt"
        "time"

        "github.com/ugorji/go/codec"
        "github.com/weaveworks/ps"
    )
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 empty_latest_map_variable="Empty${latest_map_type}"
    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 {
        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.
    type ${latest_map_type} struct { ps.Map }

    // ${empty_latest_map_variable} is an empty ${latest_map_type}. Start with this.
    var ${empty_latest_map_variable} = ${latest_map_type}{ps.NewMap()}

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

    // Copy is a noop, as ${latest_map_type}s are immutable.
    func (m ${latest_map_type}) Copy() ${latest_map_type} {
        return m
    }

    // Size returns the number of elements.
    func (m ${latest_map_type}) Size() int {
        if m.Map == nil {
	        return 0
        }
        return m.Map.Size()
    }

    // 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(other ${latest_map_type}) ${latest_map_type} {
        output := mergeMaps(m.Map, other.Map, func(a, b interface{}) bool {
	        return a.(*${entry_type}).Timestamp.Before(b.(*${entry_type}).Timestamp)
        })
        return ${latest_map_type}{output}
    }

    // 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) {
        if m.Map == nil {
	        var zero ${data_type}
	        return zero, time.Time{}, false
        }
        value, ok := m.Map.Lookup(key)
        if !ok {
	        var zero ${data_type}
	        return zero, time.Time{}, false
        }
        e := value.(*${entry_type})
        return e.Value, e.Timestamp, true
    }

    // Set the value for the given key.
    func (m ${latest_map_type}) Set(key string, timestamp time.Time, value ${data_type}) ${latest_map_type} {
        if m.Map == nil {
	        m.Map = ps.NewMap()
        }
        return ${latest_map_type}{m.Map.Set(key, &${entry_type}{Timestamp: timestamp, Value: value})}
    }

    // Delete the value for the given key.
    func (m ${latest_map_type}) Delete(key string) ${latest_map_type} {
        if m.Map == nil {
	        return m
        }
        return ${latest_map_type}{m.Map.Delete(key)}
    }

    // 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})) {
        if m.Map != nil {
	        m.Map.ForEach(func(key string, value interface{}) {
		        fn(key, value.(*${entry_type}).Timestamp, value.(*${entry_type}).Value)
	        })
        }
    }

    // String returns the ${latest_map_type}'s string representation.
    func (m ${latest_map_type}) String() string {
        return mapToString(m.Map)
    }

    // DeepEqual tests equality with other ${latest_map_type}.
    func (m ${latest_map_type}) DeepEqual(n ${latest_map_type}) bool {
        return mapEqual(m.Map, n.Map, func(val, otherValue interface{}) bool {
	        return val.(*${entry_type}).Equal(otherValue.(*${entry_type}))
        })
    }

    // CodecEncodeSelf implements codec.Selfer.
    func (m *${latest_map_type}) CodecEncodeSelf(encoder *codec.Encoder) {
        mapWrite(m.Map, encoder, func(encoder *codec.Encoder, val interface{}) {
            val.(*${entry_type}).CodecEncodeSelf(encoder)
        })
    }

    // CodecDecodeSelf implements codec.Selfer.
    func (m *${latest_map_type}) CodecDecodeSelf(decoder *codec.Decoder) {
        out := mapRead(decoder, func(isNil bool) interface{} {
	        value := &${entry_type}{}
	        if !isNil {
		        value.CodecDecodeSelf(decoder)
	        }
	        return value
        })
        *m = ${latest_map_type}{out}
    }

    // 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}"
