// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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.

package safemapstr

import (
	"strings"

	"github.com/elastic/beats/v7/libbeat/common"
)

const alternativeKey = "value"

// Put This method implements a way to put dotted keys into a MapStr while
// ensuring they don't override each other. For example:
//
//  a := MapStr{}
//  safemapstr.Put(a, "com.docker.swarm.task", "x")
//  safemapstr.Put(a, "com.docker.swarm.task.id", 1)
//  safemapstr.Put(a, "com.docker.swarm.task.name", "foobar")
//
// Will result in `{"com":{"docker":{"swarm":{"task":{"id":1,"name":"foobar","value":"x"}}}}}`
//
// Put detects this scenario and renames the common base key, by appending
// `.value`
func Put(data common.MapStr, key string, value interface{}) error {
	// XXX This implementation mimics `common.MapStr.Put`, both should be updated to have similar behavior

	d, k := mapFind(data, key, alternativeKey)
	d[k] = value
	return nil
}

// mapFind walk the map based on the given dotted key and returns the final map
// and key to operate on. This function adds intermediate maps, if the key is
// missing from the original map.

// mapFind iterates a MapStr based on the given dotted key, finding the final
// subMap and subKey to operate on.
// If a key is already used, but the used value is no map, an intermediate map will be inserted and
// the old value will be stored using the 'alternativeKey' in a new map.
// If the old value found under key is already an dictionary, subMap will be
// the old value and subKey will be set to alternativeKey.
func mapFind(data common.MapStr, key, alternativeKey string) (subMap common.MapStr, subKey string) {
	// XXX This implementation mimics `common.mapFind`, both should be updated to have similar behavior

	for {
		if oldValue, exists := data[key]; exists {
			if oldMap, ok := tryToMapStr(oldValue); ok {
				return oldMap, alternativeKey
			}
			return data, key
		}

		idx := strings.IndexRune(key, '.')
		if idx < 0 {
			// if old value exists and is a dictionary, return the old dictionary and
			// make sure we store the new value using the 'alternativeKey'
			if oldValue, exists := data[key]; exists {
				if oldMap, ok := tryToMapStr(oldValue); ok {
					return oldMap, alternativeKey
				}
			}

			return data, key
		}

		// Check if first sub-key exists. Create an intermediate map if not.
		k := key[:idx]
		d, exists := data[k]
		if !exists {
			d = common.MapStr{}
			data[k] = d
		}

		// store old value under 'alternativeKey' if the old value is no map.
		// Do not overwrite old value.
		v, ok := tryToMapStr(d)
		if !ok {
			v = common.MapStr{alternativeKey: d}
			data[k] = v
		}

		// advance into sub-map
		key = key[idx+1:]
		data = v
	}
}

func tryToMapStr(v interface{}) (common.MapStr, bool) {
	switch m := v.(type) {
	case common.MapStr:
		return m, true
	case map[string]interface{}:
		return common.MapStr(m), true
	default:
		return nil, false
	}
}
