package logger

import (
	"fmt"
	"sort"
	"sync"

	containertypes "github.com/docker/docker/api/types/container"
	"github.com/docker/docker/pkg/plugingetter"
	units "github.com/docker/go-units"
	"github.com/pkg/errors"
)

// Creator builds a logging driver instance with given context.
type Creator func(Info) (Logger, error)

// LogOptValidator checks the options specific to the underlying
// logging implementation.
type LogOptValidator func(cfg map[string]string) error

type logdriverFactory struct {
	registry     map[string]Creator
	optValidator map[string]LogOptValidator
	m            sync.Mutex
}

func (lf *logdriverFactory) list() []string {
	ls := make([]string, 0, len(lf.registry))
	lf.m.Lock()
	for name := range lf.registry {
		ls = append(ls, name)
	}
	lf.m.Unlock()
	sort.Strings(ls)
	return ls
}

// ListDrivers gets the list of registered log driver names
func ListDrivers() []string {
	return factory.list()
}

func (lf *logdriverFactory) register(name string, c Creator) error {
	if lf.driverRegistered(name) {
		return fmt.Errorf("logger: log driver named '%s' is already registered", name)
	}

	lf.m.Lock()
	lf.registry[name] = c
	lf.m.Unlock()
	return nil
}

func (lf *logdriverFactory) driverRegistered(name string) bool {
	lf.m.Lock()
	_, ok := lf.registry[name]
	lf.m.Unlock()
	if !ok {
		if pluginGetter != nil { // this can be nil when the init functions are running
			if l, _ := getPlugin(name, plugingetter.Lookup); l != nil {
				return true
			}
		}
	}
	return ok
}

func (lf *logdriverFactory) registerLogOptValidator(name string, l LogOptValidator) error {
	lf.m.Lock()
	defer lf.m.Unlock()

	if _, ok := lf.optValidator[name]; ok {
		return fmt.Errorf("logger: log validator named '%s' is already registered", name)
	}
	lf.optValidator[name] = l
	return nil
}

func (lf *logdriverFactory) get(name string) (Creator, error) {
	lf.m.Lock()
	defer lf.m.Unlock()

	c, ok := lf.registry[name]
	if ok {
		return c, nil
	}

	c, err := getPlugin(name, plugingetter.Acquire)
	return c, errors.Wrapf(err, "logger: no log driver named '%s' is registered", name)
}

func (lf *logdriverFactory) getLogOptValidator(name string) LogOptValidator {
	lf.m.Lock()
	defer lf.m.Unlock()

	c := lf.optValidator[name]
	return c
}

var factory = &logdriverFactory{registry: make(map[string]Creator), optValidator: make(map[string]LogOptValidator)} // global factory instance

// RegisterLogDriver registers the given logging driver builder with given logging
// driver name.
func RegisterLogDriver(name string, c Creator) error {
	return factory.register(name, c)
}

// RegisterLogOptValidator registers the logging option validator with
// the given logging driver name.
func RegisterLogOptValidator(name string, l LogOptValidator) error {
	return factory.registerLogOptValidator(name, l)
}

// GetLogDriver provides the logging driver builder for a logging driver name.
func GetLogDriver(name string) (Creator, error) {
	return factory.get(name)
}

var builtInLogOpts = map[string]bool{
	"mode":            true,
	"max-buffer-size": true,
}

// ValidateLogOpts checks the options for the given log driver. The
// options supported are specific to the LogDriver implementation.
func ValidateLogOpts(name string, cfg map[string]string) error {
	if name == "none" {
		return nil
	}

	switch containertypes.LogMode(cfg["mode"]) {
	case containertypes.LogModeBlocking, containertypes.LogModeNonBlock, containertypes.LogModeUnset:
	default:
		return fmt.Errorf("logger: logging mode not supported: %s", cfg["mode"])
	}

	if s, ok := cfg["max-buffer-size"]; ok {
		if containertypes.LogMode(cfg["mode"]) != containertypes.LogModeNonBlock {
			return fmt.Errorf("logger: max-buffer-size option is only supported with 'mode=%s'", containertypes.LogModeNonBlock)
		}
		if _, err := units.RAMInBytes(s); err != nil {
			return errors.Wrap(err, "error parsing option max-buffer-size")
		}
	}

	if !factory.driverRegistered(name) {
		return fmt.Errorf("logger: no log driver named '%s' is registered", name)
	}

	filteredOpts := make(map[string]string, len(builtInLogOpts))
	for k, v := range cfg {
		if !builtInLogOpts[k] {
			filteredOpts[k] = v
		}
	}

	validator := factory.getLogOptValidator(name)
	if validator != nil {
		return validator(filteredOpts)
	}
	return nil
}