package i18n

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"

	"golang.org/x/text/language"
)

// MessageFile represents a parsed message file.
type MessageFile struct {
	Path     string
	Tag      language.Tag
	Format   string
	Messages []*Message
}

// ParseMessageFileBytes returns the messages parsed from file.
func ParseMessageFileBytes(buf []byte, path string, unmarshalFuncs map[string]UnmarshalFunc) (*MessageFile, error) {
	lang, format := parsePath(path)
	tag := language.Make(lang)
	messageFile := &MessageFile{
		Path:   path,
		Tag:    tag,
		Format: format,
	}
	if len(buf) == 0 {
		return messageFile, nil
	}
	unmarshalFunc := unmarshalFuncs[messageFile.Format]
	if unmarshalFunc == nil {
		if messageFile.Format == "json" {
			unmarshalFunc = json.Unmarshal
		} else {
			return nil, fmt.Errorf("no unmarshaler registered for %s", messageFile.Format)
		}
	}
	var err error
	var raw interface{}
	if err = unmarshalFunc(buf, &raw); err != nil {
		return nil, err
	}

	m, err := isMessage(raw)
	if err != nil {
		return nil, err
	}

	if messageFile.Messages, err = recGetMessages(raw, m, true); err != nil {
		return nil, err
	}

	return messageFile, nil
}

const nestedSeparator = "."

var errInvalidTranslationFile = errors.New("invalid translation file, expected key-values, got a single value")

// recGetMessages looks for translation messages inside "raw" parameter,
// scanning nested maps using recursion.
func recGetMessages(raw interface{}, isMapMessage, isInitialCall bool) ([]*Message, error) {
	var messages []*Message
	var err error

	switch data := raw.(type) {
	case string:
		if isInitialCall {
			return nil, errInvalidTranslationFile
		}
		m, err := NewMessage(data)
		return []*Message{m}, err

	case map[string]interface{}:
		if isMapMessage {
			m, err := NewMessage(data)
			return []*Message{m}, err
		}
		messages = make([]*Message, 0, len(data))
		for id, data := range data {
			// recursively scan map items
			messages, err = addChildMessages(id, data, messages)
			if err != nil {
				return nil, err
			}
		}

	case map[interface{}]interface{}:
		if isMapMessage {
			m, err := NewMessage(data)
			return []*Message{m}, err
		}
		messages = make([]*Message, 0, len(data))
		for id, data := range data {
			strid, ok := id.(string)
			if !ok {
				return nil, fmt.Errorf("expected key to be string but got %#v", id)
			}
			// recursively scan map items
			messages, err = addChildMessages(strid, data, messages)
			if err != nil {
				return nil, err
			}
		}

	case []interface{}:
		// Backward compatibility for v1 file format.
		messages = make([]*Message, 0, len(data))
		for _, data := range data {
			// recursively scan slice items
			m, err := isMessage(data)
			if err != nil {
				return nil, err
			}
			childMessages, err := recGetMessages(data, m, false)
			if err != nil {
				return nil, err
			}
			messages = append(messages, childMessages...)
		}

	case nil:
		if isInitialCall {
			return nil, errInvalidTranslationFile
		}
		m, err := NewMessage("")
		return []*Message{m}, err

	default:
		return nil, fmt.Errorf("unsupported file format %T", raw)
	}

	return messages, nil
}

func addChildMessages(id string, data interface{}, messages []*Message) ([]*Message, error) {
	isChildMessage, err := isMessage(data)
	if err != nil {
		return nil, err
	}
	childMessages, err := recGetMessages(data, isChildMessage, false)
	if err != nil {
		return nil, err
	}
	for _, m := range childMessages {
		if isChildMessage {
			if m.ID == "" {
				m.ID = id // start with innermost key
			}
		} else {
			m.ID = id + nestedSeparator + m.ID // update ID with each nested key on the way
		}
		messages = append(messages, m)
	}
	return messages, nil
}

func parsePath(path string) (langTag, format string) {
	formatStartIdx := -1
	for i := len(path) - 1; i >= 0; i-- {
		c := path[i]
		if os.IsPathSeparator(c) {
			if formatStartIdx != -1 {
				langTag = path[i+1 : formatStartIdx]
			}
			return
		}
		if path[i] == '.' {
			if formatStartIdx != -1 {
				langTag = path[i+1 : formatStartIdx]
				return
			}
			if formatStartIdx == -1 {
				format = path[i+1:]
				formatStartIdx = i
			}
		}
	}
	if formatStartIdx != -1 {
		langTag = path[:formatStartIdx]
	}
	return
}
