/*
Package gotext implements GNU gettext utilities.

For quick/simple translations you can use the package level functions directly.

	    import (
		    "fmt"
		    "github.com/leonelquinteros/gotext"
	    )

	    func main() {
	        // Configure package
	        gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")

	        // Translate text from default domain
	        fmt.Println(gotext.Get("My text on 'domain-name' domain"))

	        // Translate text from a different domain without reconfigure
	        fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
	    }
*/
package gotext

import (
	"encoding/gob"
	"strings"
	"sync"
)

// Global environment variables
type config struct {
	sync.RWMutex

	// Path to library directory where all locale directories and Translation files are.
	library string

	// Default domain to look at when no domain is specified. Used by package level functions.
	domain string

	// Language set.
	languages []string

	// Storage for package level methods
	locales []*Locale
}

var globalConfig *config

// FallbackLocale is the default language to be used when no language is set.
var FallbackLocale = "en_US"

func init() {
	// Init default configuration
	globalConfig = &config{
		domain:    "default",
		languages: []string{FallbackLocale},
		library:   "/usr/local/share/locale",
		locales:   nil,
	}

	// Register Translator types for gob encoding
	gob.Register(TranslatorEncoding{})
}

// loadLocales creates a new Locale object for every language (specified using Configure)
// at package level based on the configuration of global configuration .
// It is called when trying to use Get or GetD methods.
func loadLocales(rebuildCache bool) {
	globalConfig.Lock()

	if globalConfig.locales == nil || rebuildCache {
		var locales []*Locale
		for _, language := range globalConfig.languages {
			locales = append(locales, NewLocale(globalConfig.library, language))
		}
		globalConfig.locales = locales
	}

	for _, locale := range globalConfig.locales {
		if _, ok := locale.Domains[globalConfig.domain]; !ok || rebuildCache {
			locale.AddDomain(globalConfig.domain)
		}
		locale.SetDomain(globalConfig.domain)
	}

	globalConfig.Unlock()
}

// GetDomain is the domain getter for the package configuration
func GetDomain() string {
	var dom string
	globalConfig.RLock()
	if globalConfig.locales != nil {
		// All locales have the same domain
		dom = globalConfig.locales[0].GetDomain()
	}
	if dom == "" {
		dom = globalConfig.domain
	}
	globalConfig.RUnlock()

	return dom
}

// SetDomain sets the name for the domain to be used at package level.
// It reloads the corresponding Translation file.
func SetDomain(dom string) {
	globalConfig.Lock()
	globalConfig.domain = dom
	if globalConfig.locales != nil {
		for _, locale := range globalConfig.locales {
			locale.SetDomain(dom)
		}
	}
	globalConfig.Unlock()

	loadLocales(true)
}

// GetLanguage returns the language gotext will translate into.
// If multiple languages have been supplied, the first one will be returned.
// If no language has been supplied, the fallback will be returned.
func GetLanguage() string {
	languages := GetLanguages()
	if len(languages) == 0 {
		return FallbackLocale
	}
	return languages[0]
}

// GetLanguages returns all languages that have been supplied.
func GetLanguages() []string {
	globalConfig.RLock()
	defer globalConfig.RUnlock()
	return globalConfig.languages
}

// SetLanguage sets the language code (or colon separated language codes) to be used at package level.
// It reloads the corresponding Translation file.
func SetLanguage(lang string) {
	globalConfig.Lock()
	var languages []string
	for _, language := range strings.Split(lang, ":") {
		languages = append(languages, SimplifiedLocale(language))
	}
	globalConfig.languages = languages
	globalConfig.Unlock()

	loadLocales(true)
}

// GetLibrary is the library getter for the package configuration
func GetLibrary() string {
	globalConfig.RLock()
	lib := globalConfig.library
	globalConfig.RUnlock()

	return lib
}

// SetLibrary sets the root path for the locale directories and files to be used at package level.
// It reloads the corresponding Translation file.
func SetLibrary(lib string) {
	globalConfig.Lock()
	globalConfig.library = lib
	globalConfig.Unlock()

	loadLocales(true)
}

// GetLocales returns the locales that have been set for the package configuration.
func GetLocales() []*Locale {
	globalConfig.RLock()
	defer globalConfig.RUnlock()
	return globalConfig.locales
}

// GetStorage is the locale storage getter for the package configuration.
//
// Deprecated: Storage has been renamed to Locale for consistency, use GetLocales instead.
func GetStorage() *Locale {
	locales := GetLocales()
	if len(locales) > 0 {
		return locales[0]
	}
	return nil
}

// SetLocales allows for overriding the global Locale objects with ones built manually with
// NewLocale(). This makes it possible to attach custom Domain objects from in-memory po/mo.
// The library, language and domain of the first Locale will set the default global configuration.
func SetLocales(locales []*Locale) {
	globalConfig.Lock()
	defer globalConfig.Unlock()

	globalConfig.locales = locales
	globalConfig.library = locales[0].path
	globalConfig.domain = locales[0].defaultDomain

	var languages []string
	for _, locale := range locales {
		languages = append(languages, locale.lang)
	}
	globalConfig.languages = languages
}

// SetStorage allows overriding the global Locale object with one built manually with NewLocale().
//
// Deprecated: Storage has been renamed to Locale for consistency, use SetLocales instead.
func SetStorage(locale *Locale) {
	SetLocales([]*Locale{locale})
}

// Configure sets all configuration variables to be used at package level and reloads the corresponding Translation file.
// It receives the library path, language code and domain name.
// This function is recommended to be used when changing more than one setting,
// as using each setter will introduce a I/O overhead because the Translation file will be loaded after each set.
func Configure(lib, lang, dom string) {
	globalConfig.Lock()
	globalConfig.library = lib
	var languages []string
	for _, language := range strings.Split(lang, ":") {
		languages = append(languages, SimplifiedLocale(language))
	}
	globalConfig.languages = languages
	globalConfig.domain = dom
	globalConfig.Unlock()

	loadLocales(true)
}

// Get uses the default domain globally set to return the corresponding Translation of a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func Get(str string, vars ...interface{}) string {
	return GetD(GetDomain(), str, vars...)
}

// GetN retrieves the (N)th plural form of Translation for the given string in the default domain.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetN(str, plural string, n int, vars ...interface{}) string {
	return GetND(GetDomain(), str, plural, n, vars...)
}

// GetD returns the corresponding Translation in the given domain for a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetD(dom, str string, vars ...interface{}) string {
	// Try to load default package Locales
	loadLocales(false)

	globalConfig.RLock()
	defer globalConfig.RUnlock()

	var tr string
	for i, locale := range globalConfig.locales {
		if _, ok := locale.Domains[dom]; !ok {
			locale.AddDomain(dom)
		}
		if !locale.IsTranslatedD(dom, str) && i < (len(globalConfig.locales)-1) {
			continue
		}
		tr = locale.GetD(dom, str, vars...)
		break
	}
	return tr
}

// GetND retrieves the (N)th plural form of Translation in the given domain for a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetND(dom, str, plural string, n int, vars ...interface{}) string {
	// Try to load default package Locales
	loadLocales(false)

	globalConfig.RLock()
	defer globalConfig.RUnlock()

	var tr string
	for i, locale := range globalConfig.locales {
		if _, ok := locale.Domains[dom]; !ok {
			locale.AddDomain(dom)
		}
		if !locale.IsTranslatedND(dom, str, n) && i < (len(globalConfig.locales)-1) {
			continue
		}
		tr = locale.GetND(dom, str, plural, n, vars...)
		break
	}
	return tr
}

// GetC uses the default domain globally set to return the corresponding Translation of the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetC(str, ctx string, vars ...interface{}) string {
	return GetDC(GetDomain(), str, ctx, vars...)
}

// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the default domain.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
	return GetNDC(GetDomain(), str, plural, n, ctx, vars...)
}

// GetDC returns the corresponding Translation in the given domain for the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetDC(dom, str, ctx string, vars ...interface{}) string {
	// Try to load default package Locales
	loadLocales(false)

	globalConfig.RLock()
	defer globalConfig.RUnlock()

	var tr string
	for i, locale := range globalConfig.locales {
		if !locale.IsTranslatedDC(dom, str, ctx) && i < (len(globalConfig.locales)-1) {
			continue
		}
		tr = locale.GetDC(dom, str, ctx, vars...)
		break
	}
	return tr
}

// GetNDC retrieves the (N)th plural form of Translation in the given domain for a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
	// Try to load default package Locales
	loadLocales(false)

	// Return Translation
	globalConfig.RLock()
	defer globalConfig.RUnlock()

	var tr string
	for i, locale := range globalConfig.locales {
		if !locale.IsTranslatedNDC(dom, str, n, ctx) && i < (len(globalConfig.locales)-1) {
			continue
		}
		tr = locale.GetNDC(dom, str, plural, n, ctx, vars...)
		break
	}
	return tr
}

// IsTranslated reports whether a string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslated(str string, langs ...string) bool {
	return IsTranslatedND(GetDomain(), str, 1, langs...)
}

// IsTranslatedN reports whether a plural string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedN(str string, n int, langs ...string) bool {
	return IsTranslatedND(GetDomain(), str, n, langs...)
}

// IsTranslatedD reports whether a domain string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedD(dom, str string, langs ...string) bool {
	return IsTranslatedND(dom, str, 1, langs...)
}

// IsTranslatedND reports whether a plural domain string is translated in any of given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedND(dom, str string, n int, langs ...string) bool {
	if len(langs) == 0 {
		langs = GetLanguages()
	}

	loadLocales(false)

	globalConfig.RLock()
	defer globalConfig.RUnlock()

	for _, lang := range langs {
		lang = SimplifiedLocale(lang)

		for _, supportedLocale := range globalConfig.locales {
			if lang != supportedLocale.GetActualLanguage(dom) {
				continue
			}
			return supportedLocale.IsTranslatedND(dom, str, n)
		}
	}
	return false
}

// IsTranslatedC reports whether a context string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedC(str, ctx string, langs ...string) bool {
	return IsTranslatedNDC(GetDomain(), str, 1, ctx, langs...)
}

// IsTranslatedNC reports whether a plural context string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedNC(str string, n int, ctx string, langs ...string) bool {
	return IsTranslatedNDC(GetDomain(), str, n, ctx, langs...)
}

// IsTranslatedDC reports whether a domain context string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedDC(dom, str, ctx string, langs ...string) bool {
	return IsTranslatedNDC(dom, str, 0, ctx, langs...)
}

// IsTranslatedNDC reports whether a plural domain context string is translated in any of given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedNDC(dom, str string, n int, ctx string, langs ...string) bool {
	if len(langs) == 0 {
		langs = GetLanguages()
	}

	loadLocales(false)

	globalConfig.RLock()
	defer globalConfig.RUnlock()

	for _, lang := range langs {
		lang = SimplifiedLocale(lang)

		for _, locale := range globalConfig.locales {
			if lang != locale.GetActualLanguage(dom) {
				continue
			}
			return locale.IsTranslatedNDC(dom, str, n, ctx)
		}
	}
	return false
}
