package imap

import (
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/textproto"
	"strings"
	"time"
)

func maybeString(mystery interface{}) string {
	if s, ok := mystery.(string); ok {
		return s
	}
	return ""
}

func convertField(f interface{}, charsetReader func(io.Reader) io.Reader) string {
	// An IMAP string contains only 7-bit data, no need to decode it
	if s, ok := f.(string); ok {
		return s
	}

	// If no charset is provided, getting directly the string is faster
	if charsetReader == nil {
		if stringer, ok := f.(fmt.Stringer); ok {
			return stringer.String()
		}
	}

	// Not a string, it must be a literal
	l, ok := f.(Literal)
	if !ok {
		return ""
	}

	var r io.Reader = l
	if charsetReader != nil {
		if dec := charsetReader(r); dec != nil {
			r = dec
		}
	}

	b, err := ioutil.ReadAll(r)
	if err != nil {
		return ""
	}
	return string(b)
}

func popSearchField(fields []interface{}) (interface{}, []interface{}, error) {
	if len(fields) == 0 {
		return nil, nil, errors.New("imap: no enough fields for search key")
	}
	return fields[0], fields[1:], nil
}

// SearchCriteria is a search criteria. A message matches the criteria if and
// only if it matches each one of its fields.
type SearchCriteria struct {
	SeqNum *SeqSet // Sequence number is in sequence set
	Uid    *SeqSet // UID is in sequence set

	// Time and timezone are ignored
	Since      time.Time // Internal date is since this date
	Before     time.Time // Internal date is before this date
	SentSince  time.Time // Date header field is since this date
	SentBefore time.Time // Date header field is before this date

	Header textproto.MIMEHeader // Each header field value is present
	Body   []string             // Each string is in the body
	Text   []string             // Each string is in the text (header + body)

	WithFlags    []string // Each flag is present
	WithoutFlags []string // Each flag is not present

	Larger  uint32 // Size is larger than this number
	Smaller uint32 // Size is smaller than this number

	Not []*SearchCriteria    // Each criteria doesn't match
	Or  [][2]*SearchCriteria // Each criteria pair has at least one match of two
}

// NewSearchCriteria creates a new search criteria.
func NewSearchCriteria() *SearchCriteria {
	return &SearchCriteria{Header: make(textproto.MIMEHeader)}
}

func (c *SearchCriteria) parseField(fields []interface{}, charsetReader func(io.Reader) io.Reader) ([]interface{}, error) {
	if len(fields) == 0 {
		return nil, nil
	}

	f := fields[0]
	fields = fields[1:]

	if subfields, ok := f.([]interface{}); ok {
		return fields, c.ParseWithCharset(subfields, charsetReader)
	}

	key, ok := f.(string)
	if !ok {
		return nil, fmt.Errorf("imap: invalid search criteria field type: %T", f)
	}
	key = strings.ToUpper(key)

	var err error
	switch key {
	case "ALL":
		// Nothing to do
	case "ANSWERED", "DELETED", "DRAFT", "FLAGGED", "RECENT", "SEEN":
		c.WithFlags = append(c.WithFlags, CanonicalFlag("\\"+key))
	case "BCC", "CC", "FROM", "SUBJECT", "TO":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		}
		if c.Header == nil {
			c.Header = make(textproto.MIMEHeader)
		}
		c.Header.Add(key, convertField(f, charsetReader))
	case "BEFORE":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
			return nil, err
		} else if c.Before.IsZero() || t.Before(c.Before) {
			c.Before = t
		}
	case "BODY":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else {
			c.Body = append(c.Body, convertField(f, charsetReader))
		}
	case "HEADER":
		var f1, f2 interface{}
		if f1, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else if f2, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else {
			if c.Header == nil {
				c.Header = make(textproto.MIMEHeader)
			}
			c.Header.Add(maybeString(f1), convertField(f2, charsetReader))
		}
	case "KEYWORD":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else {
			c.WithFlags = append(c.WithFlags, CanonicalFlag(maybeString(f)))
		}
	case "LARGER":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else if n, err := ParseNumber(f); err != nil {
			return nil, err
		} else if c.Larger == 0 || n > c.Larger {
			c.Larger = n
		}
	case "NEW":
		c.WithFlags = append(c.WithFlags, RecentFlag)
		c.WithoutFlags = append(c.WithoutFlags, SeenFlag)
	case "NOT":
		not := new(SearchCriteria)
		if fields, err = not.parseField(fields, charsetReader); err != nil {
			return nil, err
		}
		c.Not = append(c.Not, not)
	case "OLD":
		c.WithoutFlags = append(c.WithoutFlags, RecentFlag)
	case "ON":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
			return nil, err
		} else {
			c.Since = t
			c.Before = t.Add(24 * time.Hour)
		}
	case "OR":
		c1, c2 := new(SearchCriteria), new(SearchCriteria)
		if fields, err = c1.parseField(fields, charsetReader); err != nil {
			return nil, err
		} else if fields, err = c2.parseField(fields, charsetReader); err != nil {
			return nil, err
		}
		c.Or = append(c.Or, [2]*SearchCriteria{c1, c2})
	case "SENTBEFORE":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
			return nil, err
		} else if c.SentBefore.IsZero() || t.Before(c.SentBefore) {
			c.SentBefore = t
		}
	case "SENTON":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
			return nil, err
		} else {
			c.SentSince = t
			c.SentBefore = t.Add(24 * time.Hour)
		}
	case "SENTSINCE":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
			return nil, err
		} else if c.SentSince.IsZero() || t.After(c.SentSince) {
			c.SentSince = t
		}
	case "SINCE":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
			return nil, err
		} else if c.Since.IsZero() || t.After(c.Since) {
			c.Since = t
		}
	case "SMALLER":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else if n, err := ParseNumber(f); err != nil {
			return nil, err
		} else if c.Smaller == 0 || n < c.Smaller {
			c.Smaller = n
		}
	case "TEXT":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else {
			c.Text = append(c.Text, convertField(f, charsetReader))
		}
	case "UID":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else if c.Uid, err = NewSeqSet(maybeString(f)); err != nil {
			return nil, err
		}
	case "UNANSWERED", "UNDELETED", "UNDRAFT", "UNFLAGGED", "UNSEEN":
		unflag := strings.TrimPrefix(key, "UN")
		c.WithoutFlags = append(c.WithoutFlags, CanonicalFlag("\\"+unflag))
	case "UNKEYWORD":
		if f, fields, err = popSearchField(fields); err != nil {
			return nil, err
		} else {
			c.WithoutFlags = append(c.WithoutFlags, CanonicalFlag(maybeString(f)))
		}
	default: // Try to parse a sequence set
		if c.SeqNum, err = NewSeqSet(key); err != nil {
			return nil, err
		}
	}

	return fields, nil
}

// ParseWithCharset parses a search criteria from the provided fields.
// charsetReader is an optional function that converts from the fields charset
// to UTF-8.
func (c *SearchCriteria) ParseWithCharset(fields []interface{}, charsetReader func(io.Reader) io.Reader) error {
	for len(fields) > 0 {
		var err error
		if fields, err = c.parseField(fields, charsetReader); err != nil {
			return err
		}
	}
	return nil
}

// Format formats search criteria to fields. UTF-8 is used.
func (c *SearchCriteria) Format() []interface{} {
	var fields []interface{}

	if c.SeqNum != nil {
		fields = append(fields, c.SeqNum)
	}
	if c.Uid != nil {
		fields = append(fields, "UID", c.Uid)
	}

	if !c.Since.IsZero() && !c.Before.IsZero() && c.Before.Sub(c.Since) == 24*time.Hour {
		fields = append(fields, "ON", searchDate(c.Since))
	} else {
		if !c.Since.IsZero() {
			fields = append(fields, "SINCE", searchDate(c.Since))
		}
		if !c.Before.IsZero() {
			fields = append(fields, "BEFORE", searchDate(c.Before))
		}
	}
	if !c.SentSince.IsZero() && !c.SentBefore.IsZero() && c.SentBefore.Sub(c.SentSince) == 24*time.Hour {
		fields = append(fields, "SENTON", searchDate(c.SentSince))
	} else {
		if !c.SentSince.IsZero() {
			fields = append(fields, "SENTSINCE", searchDate(c.SentSince))
		}
		if !c.SentBefore.IsZero() {
			fields = append(fields, "SENTBEFORE", searchDate(c.SentBefore))
		}
	}

	for key, values := range c.Header {
		var prefields []interface{}
		switch key {
		case "Bcc", "Cc", "From", "Subject", "To":
			prefields = []interface{}{strings.ToUpper(key)}
		default:
			prefields = []interface{}{"HEADER", key}
		}
		for _, value := range values {
			fields = append(fields, prefields...)
			fields = append(fields, value)
		}
	}

	for _, value := range c.Body {
		fields = append(fields, "BODY", value)
	}
	for _, value := range c.Text {
		fields = append(fields, "TEXT", value)
	}

	for _, flag := range c.WithFlags {
		var subfields []interface{}
		switch flag {
		case AnsweredFlag, DeletedFlag, DraftFlag, FlaggedFlag, RecentFlag, SeenFlag:
			subfields = []interface{}{strings.ToUpper(strings.TrimPrefix(flag, "\\"))}
		default:
			subfields = []interface{}{"KEYWORD", flag}
		}
		fields = append(fields, subfields...)
	}
	for _, flag := range c.WithoutFlags {
		var subfields []interface{}
		switch flag {
		case AnsweredFlag, DeletedFlag, DraftFlag, FlaggedFlag, SeenFlag:
			subfields = []interface{}{"UN" + strings.ToUpper(strings.TrimPrefix(flag, "\\"))}
		case RecentFlag:
			subfields = []interface{}{"OLD"}
		default:
			subfields = []interface{}{"UNKEYWORD", flag}
		}
		fields = append(fields, subfields...)
	}

	if c.Larger > 0 {
		fields = append(fields, "LARGER", c.Larger)
	}
	if c.Smaller > 0 {
		fields = append(fields, "SMALLER", c.Smaller)
	}

	for _, not := range c.Not {
		fields = append(fields, "NOT", not.Format())
	}

	for _, or := range c.Or {
		fields = append(fields, "OR", or[0].Format(), or[1].Format())
	}

	return fields
}
