// Copyright 2016 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.

package loggo

import (
	"fmt"
	"strings"
	"sync"
)

// Context produces loggers for a hierarchy of modules. The context holds
// a collection of hierarchical loggers and their writers.
type Context struct {
	root *module

	// Perhaps have one mutex?
	modulesMutex sync.Mutex
	modules      map[string]*module

	writersMutex sync.Mutex
	writers      map[string]Writer

	// writeMuxtex is used to serialise write operations.
	writeMutex sync.Mutex
}

// NewLoggers returns a new Context with no writers set.
// If the root level is UNSPECIFIED, WARNING is used.
func NewContext(rootLevel Level) *Context {
	if rootLevel < TRACE || rootLevel > CRITICAL {
		rootLevel = WARNING
	}
	context := &Context{
		modules: make(map[string]*module),
		writers: make(map[string]Writer),
	}
	context.root = &module{
		level:   rootLevel,
		context: context,
	}
	context.modules[""] = context.root
	return context
}

// GetLogger returns a Logger for the given module name, creating it and
// its parents if necessary.
func (c *Context) GetLogger(name string) Logger {
	name = strings.TrimSpace(strings.ToLower(name))
	c.modulesMutex.Lock()
	defer c.modulesMutex.Unlock()
	return Logger{c.getLoggerModule(name)}
}

func (c *Context) getLoggerModule(name string) *module {
	if name == rootString {
		name = ""
	}
	impl, found := c.modules[name]
	if found {
		return impl
	}
	parentName := ""
	if i := strings.LastIndex(name, "."); i >= 0 {
		parentName = name[0:i]
	}
	parent := c.getLoggerModule(parentName)
	impl = &module{name, UNSPECIFIED, parent, c}
	c.modules[name] = impl
	return impl
}

// Config returns the current configuration of the Loggers. Loggers
// with UNSPECIFIED level will not be included.
func (c *Context) Config() Config {
	result := make(Config)
	c.modulesMutex.Lock()
	defer c.modulesMutex.Unlock()

	for name, module := range c.modules {
		if module.level != UNSPECIFIED {
			result[name] = module.level
		}
	}
	return result
}

// CompleteConfig returns all the loggers and their defined levels,
// even if that level is UNSPECIFIED.
func (c *Context) CompleteConfig() Config {
	result := make(Config)
	c.modulesMutex.Lock()
	defer c.modulesMutex.Unlock()

	for name, module := range c.modules {
		result[name] = module.level
	}
	return result
}

// ApplyConfig configures the logging modules according to the provided config.
func (c *Context) ApplyConfig(config Config) {
	c.modulesMutex.Lock()
	defer c.modulesMutex.Unlock()
	for name, level := range config {
		module := c.getLoggerModule(name)
		module.setLevel(level)
	}
}

// ResetLoggerLevels iterates through the known logging modules and sets the
// levels of all to UNSPECIFIED, except for <root> which is set to WARNING.
func (c *Context) ResetLoggerLevels() {
	c.modulesMutex.Lock()
	defer c.modulesMutex.Unlock()
	// Setting the root module to UNSPECIFIED will set it to WARNING.
	for _, module := range c.modules {
		module.setLevel(UNSPECIFIED)
	}
}

func (c *Context) write(entry Entry) {
	c.writeMutex.Lock()
	defer c.writeMutex.Unlock()
	for _, writer := range c.getWriters() {
		writer.Write(entry)
	}
}

func (c *Context) getWriters() []Writer {
	c.writersMutex.Lock()
	defer c.writersMutex.Unlock()
	var result []Writer
	for _, writer := range c.writers {
		result = append(result, writer)
	}
	return result
}

// AddWriter adds a writer to the list to be called for each logging call.
// The name cannot be empty, and the writer cannot be nil. If an existing
// writer exists with the specified name, an error is returned.
func (c *Context) AddWriter(name string, writer Writer) error {
	if name == "" {
		return fmt.Errorf("name cannot be empty")
	}
	if writer == nil {
		return fmt.Errorf("writer cannot be nil")
	}
	c.writersMutex.Lock()
	defer c.writersMutex.Unlock()
	if _, found := c.writers[name]; found {
		return fmt.Errorf("context already has a writer named %q", name)
	}
	c.writers[name] = writer
	return nil
}

// RemoveWriter remotes the specified writer. If a writer is not found with
// the specified name an error is returned. The writer that was removed is also
// returned.
func (c *Context) RemoveWriter(name string) (Writer, error) {
	c.writersMutex.Lock()
	defer c.writersMutex.Unlock()
	reg, found := c.writers[name]
	if !found {
		return nil, fmt.Errorf("context has no writer named %q", name)
	}
	delete(c.writers, name)
	return reg, nil
}

// ReplaceWriter is a convenience method that does the equivalent of RemoveWriter
// followed by AddWriter with the same name. The replaced writer is returned.
func (c *Context) ReplaceWriter(name string, writer Writer) (Writer, error) {
	if name == "" {
		return nil, fmt.Errorf("name cannot be empty")
	}
	if writer == nil {
		return nil, fmt.Errorf("writer cannot be nil")
	}
	c.writersMutex.Lock()
	defer c.writersMutex.Unlock()
	reg, found := c.writers[name]
	if !found {
		return nil, fmt.Errorf("context has no writer named %q", name)
	}
	oldWriter := reg
	c.writers[name] = writer
	return oldWriter, nil
}

// ResetWriters is generally only used in testing and removes all the writers.
func (c *Context) ResetWriters() {
	c.writersMutex.Lock()
	defer c.writersMutex.Unlock()
	c.writers = make(map[string]Writer)
}
