// 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 feature

import (
	"fmt"
	"reflect"
	"strings"
	"sync"

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

type mapper map[string]map[string]Featurable

// Registry implements a global FeatureRegistry for any kind of feature in beats.
// feature are grouped by namespace, a namespace is a kind of plugin like outputs, inputs, or queue.
// The feature name must be unique.
type Registry struct {
	sync.RWMutex
	namespaces mapper
	log        *logp.Logger
}

// NewRegistry returns a new registry.
func NewRegistry() *Registry {
	return &Registry{
		namespaces: make(mapper),
		log:        logp.NewLogger("registry"),
	}
}

// Register registers a new feature into a specific namespace, namespace are lazy created.
// Feature name must be unique.
func (r *Registry) Register(feature Featurable) error {
	r.Lock()
	defer r.Unlock()

	ns := normalize(feature.Namespace())
	name := normalize(feature.Name())

	if feature.Factory() == nil {
		return fmt.Errorf("feature '%s' cannot be registered with a nil factory", name)
	}

	// Lazy create namespaces
	_, found := r.namespaces[ns]
	if !found {
		r.namespaces[ns] = make(map[string]Featurable)
	}

	f, found := r.namespaces[ns][name]
	if found {
		if featuresEqual(feature, f) {
			// Allow both old style and new style of plugin to work together.
			r.log.Debugw(
				"ignoring, feature '%s' is already registered in the namespace '%s'",
				name,
				ns,
			)
			return nil
		}

		return fmt.Errorf(
			"could not register new feature '%s' in namespace '%s', feature name must be unique",
			name,
			ns,
		)
	}

	r.log.Debugw(
		"registering new feature",
		"namespace",
		ns,
		"name",
		name,
	)

	r.namespaces[ns][name] = feature

	return nil
}

// Unregister removes a feature from the registry.
func (r *Registry) Unregister(namespace, name string) error {
	r.Lock()
	defer r.Unlock()
	ns := normalize(namespace)

	v, found := r.namespaces[ns]
	if !found {
		return fmt.Errorf("unknown namespace named '%s'", ns)
	}

	_, found = v[name]
	if !found {
		return fmt.Errorf("unknown feature '%s' in namespace '%s'", name, ns)
	}

	delete(r.namespaces[ns], name)
	return nil
}

// Lookup searches for a Feature by the namespace-name pair.
func (r *Registry) Lookup(namespace, name string) (Featurable, error) {
	r.RLock()
	defer r.RUnlock()

	ns := normalize(namespace)
	n := normalize(name)

	v, found := r.namespaces[ns]
	if !found {
		return nil, fmt.Errorf("unknown namespace named '%s'", ns)
	}

	m, found := v[n]
	if !found {
		return nil, fmt.Errorf("unknown feature '%s' in namespace '%s'", n, ns)
	}

	return m, nil
}

// LookupAll returns all the features for a specific namespace.
func (r *Registry) LookupAll(namespace string) ([]Featurable, error) {
	r.RLock()
	defer r.RUnlock()

	ns := normalize(namespace)

	v, found := r.namespaces[ns]
	if !found {
		return nil, fmt.Errorf("unknown namespace named '%s'", ns)
	}

	list := make([]Featurable, len(v))
	c := 0
	for _, feature := range v {
		list[c] = feature
		c++
	}

	return list, nil
}

// Overwrite allow to replace an existing feature with a new implementation.
func (r *Registry) Overwrite(feature Featurable) error {
	_, err := r.Lookup(feature.Namespace(), feature.Name())
	if err == nil {
		err := r.Unregister(feature.Namespace(), feature.Name())
		if err != nil {
			return err
		}
	}

	return r.Register(feature)
}

// Size returns the number of registered features in the registry.
func (r *Registry) Size() int {
	r.RLock()
	defer r.RUnlock()

	c := 0
	for _, namespace := range r.namespaces {
		c += len(namespace)
	}

	return c
}

func featuresEqual(f1, f2 Featurable) bool {
	// There is no safe way to compare function in go,
	// but since the function pointers are global it should be stable.
	if f1.Name() == f2.Name() &&
		f1.Namespace() == f2.Namespace() &&
		reflect.ValueOf(f1.Factory()).Pointer() == reflect.ValueOf(f2.Factory()).Pointer() {
		return true
	}

	return false
}

func normalize(s string) string {
	return strings.ToLower(s)
}
