/*
 * Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved.
 * Licensed under the MIT License. See LICENSE file in the project root for full license information.
 */

package gotext

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"encoding/gob"
	"io/ioutil"
	"net/textproto"
	"os"
	"strconv"
	"strings"
	"sync"

	"github.com/leonelquinteros/gotext/plurals"
)

const (
	// MoMagicLittleEndian encoding
	MoMagicLittleEndian = 0x950412de
	// MoMagicBigEndian encoding
	MoMagicBigEndian = 0xde120495

	// EotSeparator msgctxt and msgid separator
	EotSeparator = "\x04"
	// NulSeparator msgid and msgstr separator
	NulSeparator = "\x00"
)

/*
Mo parses the content of any MO file and provides all the Translation functions needed.
It's the base object used by all package methods.
And it's safe for concurrent use by multiple goroutines by using the sync package for locking.

Example:

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

	func main() {
		// Create po object
		po := gotext.NewMoTranslator()

		// Parse .po file
		po.ParseFile("/path/to/po/file/translations.mo")

		// Get Translation
		fmt.Println(po.Get("Translate this"))
	}

*/
type Mo struct {
	// Headers storage
	Headers textproto.MIMEHeader

	// Language header
	Language string

	// Plural-Forms header
	PluralForms string

	// Parsed Plural-Forms header values
	nplurals    int
	plural      string
	pluralforms plurals.Expression

	// Storage
	translations map[string]*Translation
	contexts     map[string]map[string]*Translation

	// Sync Mutex
	sync.RWMutex

	// Parsing buffers
	trBuffer  *Translation
	ctxBuffer string
}

// NewMoTranslator creates a new Mo object with the Translator interface
func NewMoTranslator() Translator {
	return new(Mo)
}

// ParseFile tries to read the file by its provided path (f) and parse its content as a .po file.
func (mo *Mo) ParseFile(f string) {
	// Check if file exists
	info, err := os.Stat(f)
	if err != nil {
		return
	}

	// Check that isn't a directory
	if info.IsDir() {
		return
	}

	// Parse file content
	data, err := ioutil.ReadFile(f)
	if err != nil {
		return
	}

	mo.Parse(data)
}

// Parse loads the translations specified in the provided string (str)
func (mo *Mo) Parse(buf []byte) {
	// Lock while parsing
	mo.Lock()

	// Init storage
	if mo.translations == nil {
		mo.translations = make(map[string]*Translation)
		mo.contexts = make(map[string]map[string]*Translation)
	}

	r := bytes.NewReader(buf)

	var magicNumber uint32
	if err := binary.Read(r, binary.LittleEndian, &magicNumber); err != nil {
		return
		// return fmt.Errorf("gettext: %v", err)
	}
	var bo binary.ByteOrder
	switch magicNumber {
	case MoMagicLittleEndian:
		bo = binary.LittleEndian
	case MoMagicBigEndian:
		bo = binary.BigEndian
	default:
		return
		// return fmt.Errorf("gettext: %v", "invalid magic number")
	}

	var header struct {
		MajorVersion uint16
		MinorVersion uint16
		MsgIDCount   uint32
		MsgIDOffset  uint32
		MsgStrOffset uint32
		HashSize     uint32
		HashOffset   uint32
	}
	if err := binary.Read(r, bo, &header); err != nil {
		return
		// return fmt.Errorf("gettext: %v", err)
	}
	if v := header.MajorVersion; v != 0 && v != 1 {
		return
		// return fmt.Errorf("gettext: %v", "invalid version number")
	}
	if v := header.MinorVersion; v != 0 && v != 1 {
		return
		// return fmt.Errorf("gettext: %v", "invalid version number")
	}

	msgIDStart := make([]uint32, header.MsgIDCount)
	msgIDLen := make([]uint32, header.MsgIDCount)
	if _, err := r.Seek(int64(header.MsgIDOffset), 0); err != nil {
		return
		// return fmt.Errorf("gettext: %v", err)
	}
	for i := 0; i < int(header.MsgIDCount); i++ {
		if err := binary.Read(r, bo, &msgIDLen[i]); err != nil {
			return
			// return fmt.Errorf("gettext: %v", err)
		}
		if err := binary.Read(r, bo, &msgIDStart[i]); err != nil {
			return
			// return fmt.Errorf("gettext: %v", err)
		}
	}

	msgStrStart := make([]int32, header.MsgIDCount)
	msgStrLen := make([]int32, header.MsgIDCount)
	if _, err := r.Seek(int64(header.MsgStrOffset), 0); err != nil {
		return
		// return fmt.Errorf("gettext: %v", err)
	}
	for i := 0; i < int(header.MsgIDCount); i++ {
		if err := binary.Read(r, bo, &msgStrLen[i]); err != nil {
			return
			// return fmt.Errorf("gettext: %v", err)
		}
		if err := binary.Read(r, bo, &msgStrStart[i]); err != nil {
			return
			// return fmt.Errorf("gettext: %v", err)
		}
	}

	for i := 0; i < int(header.MsgIDCount); i++ {
		if _, err := r.Seek(int64(msgIDStart[i]), 0); err != nil {
			return
			// return fmt.Errorf("gettext: %v", err)
		}
		msgIDData := make([]byte, msgIDLen[i])
		if _, err := r.Read(msgIDData); err != nil {
			return
			// return fmt.Errorf("gettext: %v", err)
		}

		if _, err := r.Seek(int64(msgStrStart[i]), 0); err != nil {
			return
			// return fmt.Errorf("gettext: %v", err)
		}
		msgStrData := make([]byte, msgStrLen[i])
		if _, err := r.Read(msgStrData); err != nil {
			return
			// return fmt.Errorf("gettext: %v", err)
		}

		if len(msgIDData) == 0 {
			mo.addTranslation(msgIDData, msgStrData)
		} else {
			mo.addTranslation(msgIDData, msgStrData)
		}
	}

	// Unlock to parse headers
	mo.Unlock()

	// Parse headers
	mo.parseHeaders()
	return
	// return nil
}

func (mo *Mo) addTranslation(msgid, msgstr []byte) {
	translation := NewTranslation()
	var msgctxt []byte
	var msgidPlural []byte

	d := bytes.Split(msgid, []byte(EotSeparator))
	if len(d) == 1 {
		msgid = d[0]
	} else {
		msgid, msgctxt = d[1], d[0]
	}

	dd := bytes.Split(msgid, []byte(NulSeparator))
	if len(dd) > 1 {
		msgid = dd[0]
		dd = dd[1:]
	}

	translation.ID = string(msgid)

	msgidPlural = bytes.Join(dd, []byte(NulSeparator))
	if len(msgidPlural) > 0 {
		translation.PluralID = string(msgidPlural)
	}

	ddd := bytes.Split(msgstr, []byte(NulSeparator))
	if len(ddd) > 0 {
		for i, s := range ddd {
			translation.Trs[i] = string(s)
		}
	}

	if len(msgctxt) > 0 {
		// With context...
		if _, ok := mo.contexts[string(msgctxt)]; !ok {
			mo.contexts[string(msgctxt)] = make(map[string]*Translation)
		}
		mo.contexts[string(msgctxt)][translation.ID] = translation
	} else {
		mo.translations[translation.ID] = translation
	}
}

// parseHeaders retrieves data from previously parsed headers
func (mo *Mo) parseHeaders() {
	// Make sure we end with 2 carriage returns.
	raw := mo.Get("") + "\n\n"

	// Read
	reader := bufio.NewReader(strings.NewReader(raw))
	tp := textproto.NewReader(reader)

	var err error

	// Sync Headers write.
	mo.Lock()
	defer mo.Unlock()

	mo.Headers, err = tp.ReadMIMEHeader()
	if err != nil {
		return
	}

	// Get/save needed headers
	mo.Language = mo.Headers.Get("Language")
	mo.PluralForms = mo.Headers.Get("Plural-Forms")

	// Parse Plural-Forms formula
	if mo.PluralForms == "" {
		return
	}

	// Split plural form header value
	pfs := strings.Split(mo.PluralForms, ";")

	// Parse values
	for _, i := range pfs {
		vs := strings.SplitN(i, "=", 2)
		if len(vs) != 2 {
			continue
		}

		switch strings.TrimSpace(vs[0]) {
		case "nplurals":
			mo.nplurals, _ = strconv.Atoi(vs[1])

		case "plural":
			mo.plural = vs[1]

			if expr, err := plurals.Compile(mo.plural); err == nil {
				mo.pluralforms = expr
			}

		}
	}
}

// pluralForm calculates the plural form index corresponding to n.
// Returns 0 on error
func (mo *Mo) pluralForm(n int) int {
	mo.RLock()
	defer mo.RUnlock()

	// Failure fallback
	if mo.pluralforms == nil {
		/* Use the Germanic plural rule.  */
		if n == 1 {
			return 0
		}
		return 1

	}
	return mo.pluralforms.Eval(uint32(n))
}

// Get retrieves the corresponding Translation for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (mo *Mo) Get(str string, vars ...interface{}) string {
	// Sync read
	mo.RLock()
	defer mo.RUnlock()

	if mo.translations != nil {
		if _, ok := mo.translations[str]; ok {
			return Printf(mo.translations[str].Get(), vars...)
		}
	}

	// Return the same we received by default
	return Printf(str, vars...)
}

// GetN retrieves the (N)th plural form of Translation for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (mo *Mo) GetN(str, plural string, n int, vars ...interface{}) string {
	// Sync read
	mo.RLock()
	defer mo.RUnlock()

	if mo.translations != nil {
		if _, ok := mo.translations[str]; ok {
			return Printf(mo.translations[str].GetN(mo.pluralForm(n)), vars...)
		}
	}

	if n == 1 {
		return Printf(str, vars...)
	}
	return Printf(plural, vars...)
}

// GetC retrieves the corresponding Translation for a given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (mo *Mo) GetC(str, ctx string, vars ...interface{}) string {
	// Sync read
	mo.RLock()
	defer mo.RUnlock()

	if mo.contexts != nil {
		if _, ok := mo.contexts[ctx]; ok {
			if mo.contexts[ctx] != nil {
				if _, ok := mo.contexts[ctx][str]; ok {
					return Printf(mo.contexts[ctx][str].Get(), vars...)
				}
			}
		}
	}

	// Return the string we received by default
	return Printf(str, vars...)
}

// GetNC retrieves the (N)th plural form of Translation 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 (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
	// Sync read
	mo.RLock()
	defer mo.RUnlock()

	if mo.contexts != nil {
		if _, ok := mo.contexts[ctx]; ok {
			if mo.contexts[ctx] != nil {
				if _, ok := mo.contexts[ctx][str]; ok {
					return Printf(mo.contexts[ctx][str].GetN(mo.pluralForm(n)), vars...)
				}
			}
		}
	}

	if n == 1 {
		return Printf(str, vars...)
	}
	return Printf(plural, vars...)
}

// MarshalBinary implements encoding.BinaryMarshaler interface
func (mo *Mo) MarshalBinary() ([]byte, error) {
	obj := new(TranslatorEncoding)
	obj.Headers = mo.Headers
	obj.Language = mo.Language
	obj.PluralForms = mo.PluralForms
	obj.Nplurals = mo.nplurals
	obj.Plural = mo.plural
	obj.Translations = mo.translations
	obj.Contexts = mo.contexts

	var buff bytes.Buffer
	encoder := gob.NewEncoder(&buff)
	err := encoder.Encode(obj)

	return buff.Bytes(), err
}

// UnmarshalBinary implements encoding.BinaryUnmarshaler interface
func (mo *Mo) UnmarshalBinary(data []byte) error {
	buff := bytes.NewBuffer(data)
	obj := new(TranslatorEncoding)

	decoder := gob.NewDecoder(buff)
	err := decoder.Decode(obj)
	if err != nil {
		return err
	}

	mo.Headers = obj.Headers
	mo.Language = obj.Language
	mo.PluralForms = obj.PluralForms
	mo.nplurals = obj.Nplurals
	mo.plural = obj.Plural
	mo.translations = obj.Translations
	mo.contexts = obj.Contexts

	if expr, err := plurals.Compile(mo.plural); err == nil {
		mo.pluralforms = expr
	}

	return nil
}
