package imap

import (
	"reflect"
	"time"
)

// SearchOptions contains options for the SEARCH command.
type SearchOptions struct {
	// Requires IMAP4rev2 or ESEARCH
	ReturnMin   bool
	ReturnMax   bool
	ReturnAll   bool
	ReturnCount bool
	// Requires IMAP4rev2 or SEARCHRES
	ReturnSave bool
}

// SearchCriteria is a criteria for the SEARCH command.
//
// When multiple fields are populated, the result is the intersection ("and"
// function) of all messages that match the fields.
//
// And, Not and Or can be used to combine multiple criteria together. For
// instance, the following criteria matches messages not containing "hello":
//
//	SearchCriteria{Not: []SearchCriteria{{
//		Body: []string{"hello"},
//	}}}
//
// The following criteria matches messages containing either "hello" or
// "world":
//
//	SearchCriteria{Or: [][2]SearchCriteria{{
//		{Body: []string{"hello"}},
//		{Body: []string{"world"}},
//	}}}
type SearchCriteria struct {
	SeqNum []SeqSet
	UID    []UIDSet

	// Only the date is used, the time and timezone are ignored
	Since      time.Time
	Before     time.Time
	SentSince  time.Time
	SentBefore time.Time

	Header []SearchCriteriaHeaderField
	Body   []string
	Text   []string

	Flag    []Flag
	NotFlag []Flag

	Larger  int64
	Smaller int64

	Not []SearchCriteria
	Or  [][2]SearchCriteria

	ModSeq *SearchCriteriaModSeq // requires CONDSTORE
}

// And intersects two search criteria.
func (criteria *SearchCriteria) And(other *SearchCriteria) {
	criteria.SeqNum = append(criteria.SeqNum, other.SeqNum...)
	criteria.UID = append(criteria.UID, other.UID...)

	criteria.Since = intersectSince(criteria.Since, other.Since)
	criteria.Before = intersectBefore(criteria.Before, other.Before)
	criteria.SentSince = intersectSince(criteria.SentSince, other.SentSince)
	criteria.SentBefore = intersectBefore(criteria.SentBefore, other.SentBefore)

	criteria.Header = append(criteria.Header, other.Header...)
	criteria.Body = append(criteria.Body, other.Body...)
	criteria.Text = append(criteria.Text, other.Text...)

	criteria.Flag = append(criteria.Flag, other.Flag...)
	criteria.NotFlag = append(criteria.NotFlag, other.NotFlag...)

	if criteria.Larger == 0 || other.Larger > criteria.Larger {
		criteria.Larger = other.Larger
	}
	if criteria.Smaller == 0 || other.Smaller < criteria.Smaller {
		criteria.Smaller = other.Smaller
	}

	criteria.Not = append(criteria.Not, other.Not...)
	criteria.Or = append(criteria.Or, other.Or...)
}

func intersectSince(t1, t2 time.Time) time.Time {
	switch {
	case t1.IsZero():
		return t2
	case t2.IsZero():
		return t1
	case t1.After(t2):
		return t1
	default:
		return t2
	}
}

func intersectBefore(t1, t2 time.Time) time.Time {
	switch {
	case t1.IsZero():
		return t2
	case t2.IsZero():
		return t1
	case t1.Before(t2):
		return t1
	default:
		return t2
	}
}

type SearchCriteriaHeaderField struct {
	Key, Value string
}

type SearchCriteriaModSeq struct {
	ModSeq       uint64
	MetadataName string
	MetadataType SearchCriteriaMetadataType
}

type SearchCriteriaMetadataType string

const (
	SearchCriteriaMetadataAll     SearchCriteriaMetadataType = "all"
	SearchCriteriaMetadataPrivate SearchCriteriaMetadataType = "priv"
	SearchCriteriaMetadataShared  SearchCriteriaMetadataType = "shared"
)

// SearchData is the data returned by a SEARCH command.
type SearchData struct {
	All NumSet

	// requires IMAP4rev2 or ESEARCH
	Min   uint32
	Max   uint32
	Count uint32

	// requires CONDSTORE
	ModSeq uint64
}

// AllSeqNums returns All as a slice of sequence numbers.
func (data *SearchData) AllSeqNums() []uint32 {
	seqSet, ok := data.All.(SeqSet)
	if !ok {
		return nil
	}

	// Note: a dynamic sequence set would be a server bug
	nums, ok := seqSet.Nums()
	if !ok {
		panic("imap: SearchData.All is a dynamic number set")
	}
	return nums
}

// AllUIDs returns All as a slice of UIDs.
func (data *SearchData) AllUIDs() []UID {
	uidSet, ok := data.All.(UIDSet)
	if !ok {
		return nil
	}

	// Note: a dynamic sequence set would be a server bug
	uids, ok := uidSet.Nums()
	if !ok {
		panic("imap: SearchData.All is a dynamic number set")
	}
	return uids
}

// searchRes is a special empty UIDSet which can be used as a marker. It has
// a non-zero cap so that its data pointer is non-nil and can be compared.
//
// It's a UIDSet rather than a SeqSet so that it can be passed to the
// UID EXPUNGE command.
var (
	searchRes     = make(UIDSet, 0, 1)
	searchResAddr = reflect.ValueOf(searchRes).Pointer()
)

// SearchRes returns a special marker which can be used instead of a UIDSet to
// reference the last SEARCH result. On the wire, it's encoded as '$'.
//
// It requires IMAP4rev2 or the SEARCHRES extension.
func SearchRes() UIDSet {
	return searchRes
}

// IsSearchRes checks whether a sequence set is a reference to the last SEARCH
// result. See SearchRes.
func IsSearchRes(numSet NumSet) bool {
	return reflect.ValueOf(numSet).Pointer() == searchResAddr
}
