package imap

import (
	"errors"
	"strings"
	"sync"

	"github.com/emersion/go-imap/utf7"
)

// The primary mailbox, as defined in RFC 3501 section 5.1.
const InboxName = "INBOX"

// Returns the canonical form of a mailbox name. Mailbox names can be
// case-sensitive or case-insensitive depending on the backend implementation.
// The special INBOX mailbox is case-insensitive.
func CanonicalMailboxName(name string) string {
	if strings.ToUpper(name) == InboxName {
		return InboxName
	}
	return name
}

// Mailbox attributes definied in RFC 3501 section 7.2.2.
const (
	// It is not possible for any child levels of hierarchy to exist under this\
	// name; no child levels exist now and none can be created in the future.
	NoInferiorsAttr = "\\Noinferiors"
	// It is not possible to use this name as a selectable mailbox.
	NoSelectAttr = "\\Noselect"
	// The mailbox has been marked "interesting" by the server; the mailbox
	// probably contains messages that have been added since the last time the
	// mailbox was selected.
	MarkedAttr = "\\Marked"
	// The mailbox does not contain any additional messages since the last time
	// the mailbox was selected.
	UnmarkedAttr = "\\Unmarked"
)

// Basic mailbox info.
type MailboxInfo struct {
	// The mailbox attributes.
	Attributes []string
	// The server's path separator.
	Delimiter string
	// The mailbox name.
	Name string
}

// Parse mailbox info from fields.
func (info *MailboxInfo) Parse(fields []interface{}) error {
	if len(fields) < 3 {
		return errors.New("Mailbox info needs at least 3 fields")
	}

	info.Attributes, _ = ParseStringList(fields[0])

	info.Delimiter, _ = fields[1].(string)

	name, _ := fields[2].(string)
	info.Name, _ = utf7.Decoder.String(name)
	info.Name = CanonicalMailboxName(info.Name)

	return nil
}

// Format mailbox info to fields.
func (info *MailboxInfo) Format() []interface{} {
	name, _ := utf7.Encoder.String(info.Name)
	// Thunderbird doesn't understand delimiters if not quoted
	return []interface{}{FormatStringList(info.Attributes), Quoted(info.Delimiter), name}
}

// TODO: optimize this
func (info *MailboxInfo) match(name, pattern string) bool {
	i := strings.IndexAny(pattern, "*%")
	if i == -1 {
		// No more wildcards
		return name == pattern
	}

	// Get parts before and after wildcard
	chunk, wildcard, rest := pattern[0:i], pattern[i], pattern[i+1:]

	// Check that name begins with chunk
	if len(chunk) > 0 && !strings.HasPrefix(name, chunk) {
		return false
	}
	name = strings.TrimPrefix(name, chunk)

	// Expand wildcard
	var j int
	for j = 0; j < len(name); j++ {
		if wildcard == '%' && string(name[j]) == info.Delimiter {
			break // Stop on delimiter if wildcard is %
		}
		// Try to match the rest from here
		if info.match(name[j:], rest) {
			return true
		}
	}

	return info.match(name[j:], rest)
}

// Match checks if a reference and a pattern matches this mailbox name, as
// defined in RFC 3501 section 6.3.8.
func (info *MailboxInfo) Match(reference, pattern string) bool {
	name := info.Name

	if strings.HasPrefix(pattern, info.Delimiter) {
		reference = ""
		pattern = strings.TrimPrefix(pattern, info.Delimiter)
	}
	if reference != "" {
		if !strings.HasSuffix(reference, info.Delimiter) {
			reference += info.Delimiter
		}
		if !strings.HasPrefix(name, reference) {
			return false
		}
		name = strings.TrimPrefix(name, reference)
	}

	return info.match(name, pattern)
}

// Mailbox status items.
const (
	MailboxFlags          = "FLAGS"
	MailboxPermanentFlags = "PERMANENTFLAGS"

	// Defined in RFC 3501 section 6.3.10.
	MailboxMessages    = "MESSAGES"
	MailboxRecent      = "RECENT"
	MailboxUnseen      = "UNSEEN"
	MailboxUidNext     = "UIDNEXT"
	MailboxUidValidity = "UIDVALIDITY"
)

// A mailbox status.
type MailboxStatus struct {
	// The mailbox name.
	Name string
	// True if the mailbox is open in read-only mode.
	ReadOnly bool
	// The mailbox items that are currently filled in. This map's values
	// should not be used directly, they must only be used by libraries
	// implementing extensions of the IMAP protocol.
	Items map[string]interface{}

	// The Items map may be accessed in different goroutines. Protect
	// concurrent writes.
	ItemsLocker sync.Mutex

	// The mailbox flags.
	Flags []string
	// The mailbox permanent flags.
	PermanentFlags []string

	// The number of messages in this mailbox.
	Messages uint32
	// The number of messages not seen since the last time the mailbox was opened.
	Recent uint32
	// The number of unread messages.
	Unseen uint32
	// The next UID.
	UidNext uint32
	// Together with a UID, it is a unique identifier for a message.
	// Must be greater than or equal to 1.
	UidValidity uint32
}

// Create a new mailbox status that will contain the specified items.
func NewMailboxStatus(name string, items []string) *MailboxStatus {
	status := &MailboxStatus{
		Name:  name,
		Items: make(map[string]interface{}),
	}

	for _, k := range items {
		status.Items[k] = nil
	}

	return status
}

func (status *MailboxStatus) Parse(fields []interface{}) error {
	status.Items = make(map[string]interface{})

	var k string
	for i, f := range fields {
		if i%2 == 0 {
			var ok bool
			if k, ok = f.(string); !ok {
				return errors.New("Key is not a string")
			}
			k = strings.ToUpper(k)
		} else {
			status.Items[k] = nil

			var err error
			switch k {
			case MailboxMessages:
				status.Messages, err = ParseNumber(f)
			case MailboxRecent:
				status.Recent, err = ParseNumber(f)
			case MailboxUnseen:
				status.Unseen, err = ParseNumber(f)
			case MailboxUidNext:
				status.UidNext, err = ParseNumber(f)
			case MailboxUidValidity:
				status.UidValidity, err = ParseNumber(f)
			default:
				status.Items[k] = f
			}

			if err != nil {
				return err
			}
		}
	}

	return nil
}

func (status *MailboxStatus) Format() []interface{} {
	var fields []interface{}
	for k, v := range status.Items {
		switch k {
		case MailboxMessages:
			v = status.Messages
		case MailboxRecent:
			v = status.Recent
		case MailboxUnseen:
			v = status.Unseen
		case MailboxUidNext:
			v = status.UidNext
		case MailboxUidValidity:
			v = status.UidValidity
		}

		fields = append(fields, k, v)
	}
	return fields
}
