package imapclient

import (
	"fmt"
	"io"
	netmail "net/mail"
	"strings"
	"time"

	"github.com/emersion/go-imap/v2"
	"github.com/emersion/go-imap/v2/internal"
	"github.com/emersion/go-imap/v2/internal/imapwire"
	"github.com/emersion/go-message/mail"
)

// Fetch sends a FETCH command.
//
// The caller must fully consume the FetchCommand. A simple way to do so is to
// defer a call to FetchCommand.Close.
//
// A nil options pointer is equivalent to a zero options value.
func (c *Client) Fetch(numSet imap.NumSet, options *imap.FetchOptions) *FetchCommand {
	if options == nil {
		options = new(imap.FetchOptions)
	}

	numKind := imapwire.NumSetKind(numSet)

	cmd := &FetchCommand{
		numSet: numSet,
		msgs:   make(chan *FetchMessageData, 128),
	}
	enc := c.beginCommand(uidCmdName("FETCH", numKind), cmd)
	enc.SP().NumSet(numSet).SP()
	writeFetchItems(enc.Encoder, numKind, options)
	if options.ChangedSince != 0 {
		enc.SP().Special('(').Atom("CHANGEDSINCE").SP().ModSeq(options.ChangedSince).Special(')')
	}
	enc.end()
	return cmd
}

func writeFetchItems(enc *imapwire.Encoder, numKind imapwire.NumKind, options *imap.FetchOptions) {
	listEnc := enc.BeginList()

	// Ensure we request UID as the first data item for UID FETCH, to be safer.
	// We want to get it before any literal.
	if options.UID || numKind == imapwire.NumKindUID {
		listEnc.Item().Atom("UID")
	}

	m := map[string]bool{
		"BODY":          options.BodyStructure != nil && !options.BodyStructure.Extended,
		"BODYSTRUCTURE": options.BodyStructure != nil && options.BodyStructure.Extended,
		"ENVELOPE":      options.Envelope,
		"FLAGS":         options.Flags,
		"INTERNALDATE":  options.InternalDate,
		"RFC822.SIZE":   options.RFC822Size,
		"MODSEQ":        options.ModSeq,
	}
	for k, req := range m {
		if req {
			listEnc.Item().Atom(k)
		}
	}

	for _, bs := range options.BodySection {
		writeFetchItemBodySection(listEnc.Item(), bs)
	}
	for _, bs := range options.BinarySection {
		writeFetchItemBinarySection(listEnc.Item(), bs)
	}
	for _, bss := range options.BinarySectionSize {
		writeFetchItemBinarySectionSize(listEnc.Item(), bss)
	}

	listEnc.End()
}

func writeFetchItemBodySection(enc *imapwire.Encoder, item *imap.FetchItemBodySection) {
	enc.Atom("BODY")
	if item.Peek {
		enc.Atom(".PEEK")
	}
	enc.Special('[')
	writeSectionPart(enc, item.Part)
	if len(item.Part) > 0 && item.Specifier != imap.PartSpecifierNone {
		enc.Special('.')
	}
	if item.Specifier != imap.PartSpecifierNone {
		enc.Atom(string(item.Specifier))

		var headerList []string
		if len(item.HeaderFields) > 0 {
			headerList = item.HeaderFields
			enc.Atom(".FIELDS")
		} else if len(item.HeaderFieldsNot) > 0 {
			headerList = item.HeaderFieldsNot
			enc.Atom(".FIELDS.NOT")
		}

		if len(headerList) > 0 {
			enc.SP().List(len(headerList), func(i int) {
				enc.String(headerList[i])
			})
		}
	}
	enc.Special(']')
	writeSectionPartial(enc, item.Partial)
}

func writeFetchItemBinarySection(enc *imapwire.Encoder, item *imap.FetchItemBinarySection) {
	enc.Atom("BINARY")
	if item.Peek {
		enc.Atom(".PEEK")
	}
	enc.Special('[')
	writeSectionPart(enc, item.Part)
	enc.Special(']')
	writeSectionPartial(enc, item.Partial)
}

func writeFetchItemBinarySectionSize(enc *imapwire.Encoder, item *imap.FetchItemBinarySectionSize) {
	enc.Atom("BINARY.SIZE")
	enc.Special('[')
	writeSectionPart(enc, item.Part)
	enc.Special(']')
}

func writeSectionPart(enc *imapwire.Encoder, part []int) {
	if len(part) == 0 {
		return
	}

	var l []string
	for _, num := range part {
		l = append(l, fmt.Sprintf("%v", num))
	}
	enc.Atom(strings.Join(l, "."))
}

func writeSectionPartial(enc *imapwire.Encoder, partial *imap.SectionPartial) {
	if partial == nil {
		return
	}
	enc.Special('<').Number64(partial.Offset).Special('.').Number64(partial.Size).Special('>')
}

// FetchCommand is a FETCH command.
type FetchCommand struct {
	commandBase

	numSet     imap.NumSet
	recvSeqSet imap.SeqSet
	recvUIDSet imap.UIDSet

	msgs chan *FetchMessageData
	prev *FetchMessageData
}

func (cmd *FetchCommand) recvSeqNum(seqNum uint32) bool {
	set, ok := cmd.numSet.(imap.SeqSet)
	if !ok || !set.Contains(seqNum) {
		return false
	}

	if cmd.recvSeqSet.Contains(seqNum) {
		return false
	}

	cmd.recvSeqSet.AddNum(seqNum)
	return true
}

func (cmd *FetchCommand) recvUID(uid imap.UID) bool {
	set, ok := cmd.numSet.(imap.UIDSet)
	if !ok || !set.Contains(uid) {
		return false
	}

	if cmd.recvUIDSet.Contains(uid) {
		return false
	}

	cmd.recvUIDSet.AddNum(uid)
	return true
}

// Next advances to the next message.
//
// On success, the message is returned. On error or if there are no more
// messages, nil is returned. To check the error value, use Close.
func (cmd *FetchCommand) Next() *FetchMessageData {
	if cmd.prev != nil {
		cmd.prev.discard()
	}
	cmd.prev = <-cmd.msgs
	return cmd.prev
}

// Close releases the command.
//
// Calling Close unblocks the IMAP client decoder and lets it read the next
// responses. Next will always return nil after Close.
func (cmd *FetchCommand) Close() error {
	for cmd.Next() != nil {
		// ignore
	}
	return cmd.wait()
}

// Collect accumulates message data into a list.
//
// This method will read and store message contents in memory. This is
// acceptable when the message contents have a reasonable size, but may not be
// suitable when fetching e.g. attachments.
//
// This is equivalent to calling Next repeatedly and then Close.
func (cmd *FetchCommand) Collect() ([]*FetchMessageBuffer, error) {
	defer cmd.Close()

	var l []*FetchMessageBuffer
	for {
		msg := cmd.Next()
		if msg == nil {
			break
		}

		buf, err := msg.Collect()
		if err != nil {
			return l, err
		}

		l = append(l, buf)
	}
	return l, cmd.Close()
}

func matchFetchItemBodySection(cmd, resp *imap.FetchItemBodySection) bool {
	if cmd.Specifier != resp.Specifier {
		return false
	}

	if !intSliceEqual(cmd.Part, resp.Part) {
		return false
	}
	if !stringSliceEqualFold(cmd.HeaderFields, resp.HeaderFields) {
		return false
	}
	if !stringSliceEqualFold(cmd.HeaderFieldsNot, resp.HeaderFieldsNot) {
		return false
	}

	if (cmd.Partial == nil) != (resp.Partial == nil) {
		return false
	}
	if cmd.Partial != nil && cmd.Partial.Offset != resp.Partial.Offset {
		return false
	}

	// Ignore Partial.Size and Peek: these are not echoed back by the server
	return true
}

func matchFetchItemBinarySection(cmd, resp *imap.FetchItemBinarySection) bool {
	// Ignore Partial and Peek: these are not echoed back by the server
	return intSliceEqual(cmd.Part, resp.Part)
}

func intSliceEqual(a, b []int) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

func stringSliceEqualFold(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if !strings.EqualFold(a[i], b[i]) {
			return false
		}
	}
	return true
}

// FetchMessageData contains a message's FETCH data.
type FetchMessageData struct {
	SeqNum uint32

	items chan FetchItemData
	prev  FetchItemData
}

// Next advances to the next data item for this message.
//
// If there is one or more data items left, the next item is returned.
// Otherwise nil is returned.
func (data *FetchMessageData) Next() FetchItemData {
	if d, ok := data.prev.(discarder); ok {
		d.discard()
	}

	item := <-data.items
	data.prev = item
	return item
}

func (data *FetchMessageData) discard() {
	for {
		if item := data.Next(); item == nil {
			break
		}
	}
}

// Collect accumulates message data into a struct.
//
// This method will read and store message contents in memory. This is
// acceptable when the message contents have a reasonable size, but may not be
// suitable when fetching e.g. attachments.
func (data *FetchMessageData) Collect() (*FetchMessageBuffer, error) {
	defer data.discard()

	buf := &FetchMessageBuffer{SeqNum: data.SeqNum}
	for {
		item := data.Next()
		if item == nil {
			break
		}
		if err := buf.populateItemData(item); err != nil {
			return buf, err
		}
	}
	return buf, nil
}

// FetchItemData contains a message's FETCH item data.
type FetchItemData interface {
	fetchItemData()
}

var (
	_ FetchItemData = FetchItemDataBodySection{}
	_ FetchItemData = FetchItemDataBinarySection{}
	_ FetchItemData = FetchItemDataFlags{}
	_ FetchItemData = FetchItemDataEnvelope{}
	_ FetchItemData = FetchItemDataInternalDate{}
	_ FetchItemData = FetchItemDataRFC822Size{}
	_ FetchItemData = FetchItemDataUID{}
	_ FetchItemData = FetchItemDataBodyStructure{}
)

type discarder interface {
	discard()
}

var (
	_ discarder = FetchItemDataBodySection{}
	_ discarder = FetchItemDataBinarySection{}
)

// FetchItemDataBodySection holds data returned by FETCH BODY[].
//
// Literal might be nil.
type FetchItemDataBodySection struct {
	Section *imap.FetchItemBodySection
	Literal imap.LiteralReader
}

func (FetchItemDataBodySection) fetchItemData() {}

func (item FetchItemDataBodySection) discard() {
	if item.Literal != nil {
		io.Copy(io.Discard, item.Literal)
	}
}

// MatchCommand checks whether a section returned by the server in a response
// is compatible with a section requested by the client in a command.
func (dataItem *FetchItemDataBodySection) MatchCommand(item *imap.FetchItemBodySection) bool {
	return matchFetchItemBodySection(item, dataItem.Section)
}

// FetchItemDataBinarySection holds data returned by FETCH BINARY[].
//
// Literal might be nil.
type FetchItemDataBinarySection struct {
	Section *imap.FetchItemBinarySection
	Literal imap.LiteralReader
}

func (FetchItemDataBinarySection) fetchItemData() {}

func (item FetchItemDataBinarySection) discard() {
	if item.Literal != nil {
		io.Copy(io.Discard, item.Literal)
	}
}

// MatchCommand checks whether a section returned by the server in a response
// is compatible with a section requested by the client in a command.
func (dataItem *FetchItemDataBinarySection) MatchCommand(item *imap.FetchItemBinarySection) bool {
	return matchFetchItemBinarySection(item, dataItem.Section)
}

// FetchItemDataFlags holds data returned by FETCH FLAGS.
type FetchItemDataFlags struct {
	Flags []imap.Flag
}

func (FetchItemDataFlags) fetchItemData() {}

// FetchItemDataEnvelope holds data returned by FETCH ENVELOPE.
type FetchItemDataEnvelope struct {
	Envelope *imap.Envelope
}

func (FetchItemDataEnvelope) fetchItemData() {}

// FetchItemDataInternalDate holds data returned by FETCH INTERNALDATE.
type FetchItemDataInternalDate struct {
	Time time.Time
}

func (FetchItemDataInternalDate) fetchItemData() {}

// FetchItemDataRFC822Size holds data returned by FETCH RFC822.SIZE.
type FetchItemDataRFC822Size struct {
	Size int64
}

func (FetchItemDataRFC822Size) fetchItemData() {}

// FetchItemDataUID holds data returned by FETCH UID.
type FetchItemDataUID struct {
	UID imap.UID
}

func (FetchItemDataUID) fetchItemData() {}

// FetchItemDataBodyStructure holds data returned by FETCH BODYSTRUCTURE or
// FETCH BODY.
type FetchItemDataBodyStructure struct {
	BodyStructure imap.BodyStructure
	IsExtended    bool // True if BODYSTRUCTURE, false if BODY
}

func (FetchItemDataBodyStructure) fetchItemData() {}

// FetchItemDataBinarySectionSize holds data returned by FETCH BINARY.SIZE[].
type FetchItemDataBinarySectionSize struct {
	Part []int
	Size uint32
}

func (FetchItemDataBinarySectionSize) fetchItemData() {}

// MatchCommand checks whether a section size returned by the server in a
// response is compatible with a section size requested by the client in a
// command.
func (data *FetchItemDataBinarySectionSize) MatchCommand(item *imap.FetchItemBinarySectionSize) bool {
	return intSliceEqual(item.Part, data.Part)
}

// FetchItemDataModSeq holds data returned by FETCH MODSEQ.
//
// This requires the CONDSTORE extension.
type FetchItemDataModSeq struct {
	ModSeq uint64
}

func (FetchItemDataModSeq) fetchItemData() {}

// FetchBodySectionBuffer is a buffer for the data returned by
// FetchItemBodySection.
type FetchBodySectionBuffer struct {
	Section *imap.FetchItemBodySection
	Bytes   []byte
}

// FetchBinarySectionBuffer is a buffer for the data returned by
// FetchItemBinarySection.
type FetchBinarySectionBuffer struct {
	Section *imap.FetchItemBinarySection
	Bytes   []byte
}

// FetchMessageBuffer is a buffer for the data returned by FetchMessageData.
//
// The SeqNum field is always populated. All remaining fields are optional.
type FetchMessageBuffer struct {
	SeqNum            uint32
	Flags             []imap.Flag
	Envelope          *imap.Envelope
	InternalDate      time.Time
	RFC822Size        int64
	UID               imap.UID
	BodyStructure     imap.BodyStructure
	BodySection       []FetchBodySectionBuffer
	BinarySection     []FetchBinarySectionBuffer
	BinarySectionSize []FetchItemDataBinarySectionSize
	ModSeq            uint64 // requires CONDSTORE
}

func (buf *FetchMessageBuffer) populateItemData(item FetchItemData) error {
	switch item := item.(type) {
	case FetchItemDataBodySection:
		var b []byte
		if item.Literal != nil {
			var err error
			b, err = io.ReadAll(item.Literal)
			if err != nil {
				return err
			}
		}
		buf.BodySection = append(buf.BodySection, FetchBodySectionBuffer{
			Section: item.Section,
			Bytes:   b,
		})
	case FetchItemDataBinarySection:
		var b []byte
		if item.Literal != nil {
			var err error
			b, err = io.ReadAll(item.Literal)
			if err != nil {
				return err
			}
		}
		buf.BinarySection = append(buf.BinarySection, FetchBinarySectionBuffer{
			Section: item.Section,
			Bytes:   b,
		})
	case FetchItemDataFlags:
		buf.Flags = item.Flags
	case FetchItemDataEnvelope:
		buf.Envelope = item.Envelope
	case FetchItemDataInternalDate:
		buf.InternalDate = item.Time
	case FetchItemDataRFC822Size:
		buf.RFC822Size = item.Size
	case FetchItemDataUID:
		buf.UID = item.UID
	case FetchItemDataBodyStructure:
		buf.BodyStructure = item.BodyStructure
	case FetchItemDataBinarySectionSize:
		buf.BinarySectionSize = append(buf.BinarySectionSize, item)
	case FetchItemDataModSeq:
		buf.ModSeq = item.ModSeq
	default:
		panic(fmt.Errorf("unsupported fetch item data %T", item))
	}
	return nil
}

// FindBodySection returns the contents of a requested body section.
//
// If the body section is not found, nil is returned.
func (buf *FetchMessageBuffer) FindBodySection(section *imap.FetchItemBodySection) []byte {
	for _, s := range buf.BodySection {
		if matchFetchItemBodySection(section, s.Section) {
			return s.Bytes
		}
	}
	return nil
}

// FindBinarySection returns the contents of a requested binary section.
//
// If the binary section is not found, nil is returned.
func (buf *FetchMessageBuffer) FindBinarySection(section *imap.FetchItemBinarySection) []byte {
	for _, s := range buf.BinarySection {
		if matchFetchItemBinarySection(section, s.Section) {
			return s.Bytes
		}
	}
	return nil
}

// FindBinarySectionSize returns a requested binary section size.
//
// If the binary section size is not found, false is returned.
func (buf *FetchMessageBuffer) FindBinarySectionSize(part []int) (uint32, bool) {
	for _, s := range buf.BinarySectionSize {
		if intSliceEqual(part, s.Part) {
			return s.Size, true
		}
	}
	return 0, false
}

func (c *Client) handleFetch(seqNum uint32) error {
	dec := c.dec

	items := make(chan FetchItemData, 32)
	defer close(items)

	msg := &FetchMessageData{SeqNum: seqNum, items: items}

	// We're in a tricky situation: to know whether this FETCH response needs
	// to be handled by a pending command, we may need to look at the UID in
	// the response data. But the response data comes in in a streaming
	// fashion: it can contain literals. Assume that the UID will be returned
	// before any literal.
	var uid imap.UID
	handled := false
	handleMsg := func() {
		if handled {
			return
		}

		cmd := c.findPendingCmdFunc(func(anyCmd command) bool {
			cmd, ok := anyCmd.(*FetchCommand)
			if !ok {
				return false
			}

			// Skip if we haven't requested or already handled this message
			if _, ok := cmd.numSet.(imap.UIDSet); ok {
				return uid != 0 && cmd.recvUID(uid)
			} else {
				return seqNum != 0 && cmd.recvSeqNum(seqNum)
			}
		})
		if cmd != nil {
			cmd := cmd.(*FetchCommand)
			cmd.msgs <- msg
		} else if handler := c.options.unilateralDataHandler().Fetch; handler != nil {
			go handler(msg)
		} else {
			go msg.discard()
		}

		handled = true
	}
	defer handleMsg()

	numAtts := 0
	return dec.ExpectList(func() error {
		var attName string
		if !dec.Expect(dec.Func(&attName, isMsgAttNameChar), "msg-att name") {
			return dec.Err()
		}
		attName = strings.ToUpper(attName)

		var (
			item FetchItemData
			done chan struct{}
		)
		switch attName {
		case "FLAGS":
			if !dec.ExpectSP() {
				return dec.Err()
			}

			flags, err := internal.ExpectFlagList(dec)
			if err != nil {
				return err
			}

			item = FetchItemDataFlags{Flags: flags}
		case "ENVELOPE":
			if !dec.ExpectSP() {
				return dec.Err()
			}

			envelope, err := readEnvelope(dec, &c.options)
			if err != nil {
				return fmt.Errorf("in envelope: %v", err)
			}

			item = FetchItemDataEnvelope{Envelope: envelope}
		case "INTERNALDATE":
			if !dec.ExpectSP() {
				return dec.Err()
			}

			t, err := internal.ExpectDateTime(dec)
			if err != nil {
				return err
			}

			item = FetchItemDataInternalDate{Time: t}
		case "RFC822.SIZE":
			var size int64
			if !dec.ExpectSP() || !dec.ExpectNumber64(&size) {
				return dec.Err()
			}

			item = FetchItemDataRFC822Size{Size: size}
		case "UID":
			if !dec.ExpectSP() || !dec.ExpectUID(&uid) {
				return dec.Err()
			}

			item = FetchItemDataUID{UID: uid}
		case "BODY", "BINARY":
			if dec.Special('[') {
				var section interface{}
				switch attName {
				case "BODY":
					var err error
					section, err = readSectionSpec(dec)
					if err != nil {
						return fmt.Errorf("in section-spec: %v", err)
					}
				case "BINARY":
					part, dot := readSectionPart(dec)
					if dot {
						return fmt.Errorf("in section-binary: expected number after dot")
					}
					if !dec.ExpectSpecial(']') {
						return dec.Err()
					}
					section = &imap.FetchItemBinarySection{Part: part}
				}

				if !dec.ExpectSP() {
					return dec.Err()
				}

				// Ignore literal8 marker, if any
				if attName == "BINARY" {
					dec.Special('~')
				}

				lit, _, ok := dec.ExpectNStringReader()
				if !ok {
					return dec.Err()
				}

				var fetchLit imap.LiteralReader
				if lit != nil {
					done = make(chan struct{})
					fetchLit = &fetchLiteralReader{
						LiteralReader: lit,
						ch:            done,
					}
				}

				switch section := section.(type) {
				case *imap.FetchItemBodySection:
					item = FetchItemDataBodySection{
						Section: section,
						Literal: fetchLit,
					}
				case *imap.FetchItemBinarySection:
					item = FetchItemDataBinarySection{
						Section: section,
						Literal: fetchLit,
					}
				}
				break
			}
			if !dec.Expect(attName == "BODY", "'['") {
				return dec.Err()
			}
			fallthrough
		case "BODYSTRUCTURE":
			if !dec.ExpectSP() {
				return dec.Err()
			}

			bodyStruct, err := readBody(dec, &c.options)
			if err != nil {
				return err
			}

			item = FetchItemDataBodyStructure{
				BodyStructure: bodyStruct,
				IsExtended:    attName == "BODYSTRUCTURE",
			}
		case "BINARY.SIZE":
			if !dec.ExpectSpecial('[') {
				return dec.Err()
			}
			part, dot := readSectionPart(dec)
			if dot {
				return fmt.Errorf("in section-binary: expected number after dot")
			}

			var size uint32
			if !dec.ExpectSpecial(']') || !dec.ExpectSP() || !dec.ExpectNumber(&size) {
				return dec.Err()
			}

			item = FetchItemDataBinarySectionSize{
				Part: part,
				Size: size,
			}
		case "MODSEQ":
			var modSeq uint64
			if !dec.ExpectSP() || !dec.ExpectSpecial('(') || !dec.ExpectModSeq(&modSeq) || !dec.ExpectSpecial(')') {
				return dec.Err()
			}
			item = FetchItemDataModSeq{ModSeq: modSeq}
		default:
			return fmt.Errorf("unsupported msg-att name: %q", attName)
		}

		numAtts++
		if numAtts > cap(items) || done != nil {
			// To avoid deadlocking we need to ask the message handler to
			// consume the data
			handleMsg()
		}

		if done != nil {
			c.setReadTimeout(literalReadTimeout)
		}
		items <- item
		if done != nil {
			<-done
			c.setReadTimeout(respReadTimeout)
		}
		return nil
	})
}

func isMsgAttNameChar(ch byte) bool {
	return ch != '[' && imapwire.IsAtomChar(ch)
}

func readEnvelope(dec *imapwire.Decoder, options *Options) (*imap.Envelope, error) {
	var envelope imap.Envelope

	if !dec.ExpectSpecial('(') {
		return nil, dec.Err()
	}

	var date, subject string
	if !dec.ExpectNString(&date) || !dec.ExpectSP() || !dec.ExpectNString(&subject) || !dec.ExpectSP() {
		return nil, dec.Err()
	}
	// TODO: handle error
	envelope.Date, _ = netmail.ParseDate(date)
	envelope.Subject, _ = options.decodeText(subject)

	addrLists := []struct {
		name string
		out  *[]imap.Address
	}{
		{"env-from", &envelope.From},
		{"env-sender", &envelope.Sender},
		{"env-reply-to", &envelope.ReplyTo},
		{"env-to", &envelope.To},
		{"env-cc", &envelope.Cc},
		{"env-bcc", &envelope.Bcc},
	}
	for _, addrList := range addrLists {
		l, err := readAddressList(dec, options)
		if err != nil {
			return nil, fmt.Errorf("in %v: %v", addrList.name, err)
		} else if !dec.ExpectSP() {
			return nil, dec.Err()
		}
		*addrList.out = l
	}

	var inReplyTo, messageID string
	if !dec.ExpectNString(&inReplyTo) || !dec.ExpectSP() || !dec.ExpectNString(&messageID) {
		return nil, dec.Err()
	}
	// TODO: handle errors
	envelope.InReplyTo, _ = parseMsgIDList(inReplyTo)
	envelope.MessageID, _ = parseMsgID(messageID)

	if !dec.ExpectSpecial(')') {
		return nil, dec.Err()
	}
	return &envelope, nil
}

func readAddressList(dec *imapwire.Decoder, options *Options) ([]imap.Address, error) {
	var l []imap.Address
	err := dec.ExpectNList(func() error {
		addr, err := readAddress(dec, options)
		if err != nil {
			return err
		}
		l = append(l, *addr)
		return nil
	})
	return l, err
}

func readAddress(dec *imapwire.Decoder, options *Options) (*imap.Address, error) {
	var (
		addr     imap.Address
		name     string
		obsRoute string
	)
	ok := dec.ExpectSpecial('(') &&
		dec.ExpectNString(&name) && dec.ExpectSP() &&
		dec.ExpectNString(&obsRoute) && dec.ExpectSP() &&
		dec.ExpectNString(&addr.Mailbox) && dec.ExpectSP() &&
		dec.ExpectNString(&addr.Host) && dec.ExpectSpecial(')')
	if !ok {
		return nil, fmt.Errorf("in address: %v", dec.Err())
	}
	// TODO: handle error
	addr.Name, _ = options.decodeText(name)
	return &addr, nil
}

func parseMsgID(s string) (string, error) {
	var h mail.Header
	h.Set("Message-Id", s)
	return h.MessageID()
}

func parseMsgIDList(s string) ([]string, error) {
	var h mail.Header
	h.Set("In-Reply-To", s)
	return h.MsgIDList("In-Reply-To")
}

func readBody(dec *imapwire.Decoder, options *Options) (imap.BodyStructure, error) {
	if !dec.ExpectSpecial('(') {
		return nil, dec.Err()
	}

	var (
		mediaType string
		token     string
		bs        imap.BodyStructure
		err       error
	)
	if dec.String(&mediaType) {
		token = "body-type-1part"
		bs, err = readBodyType1part(dec, mediaType, options)
	} else {
		token = "body-type-mpart"
		bs, err = readBodyTypeMpart(dec, options)
	}
	if err != nil {
		return nil, fmt.Errorf("in %v: %v", token, err)
	}

	for dec.SP() {
		if !dec.DiscardValue() {
			return nil, dec.Err()
		}
	}

	if !dec.ExpectSpecial(')') {
		return nil, dec.Err()
	}

	return bs, nil
}

func readBodyType1part(dec *imapwire.Decoder, typ string, options *Options) (*imap.BodyStructureSinglePart, error) {
	bs := imap.BodyStructureSinglePart{Type: typ}

	if !dec.ExpectSP() || !dec.ExpectString(&bs.Subtype) || !dec.ExpectSP() {
		return nil, dec.Err()
	}
	var err error
	bs.Params, err = readBodyFldParam(dec, options)
	if err != nil {
		return nil, err
	}

	var description string
	if !dec.ExpectSP() || !dec.ExpectNString(&bs.ID) || !dec.ExpectSP() || !dec.ExpectNString(&description) || !dec.ExpectSP() || !dec.ExpectNString(&bs.Encoding) || !dec.ExpectSP() || !dec.ExpectBodyFldOctets(&bs.Size) {
		return nil, dec.Err()
	}

	// Content-Transfer-Encoding should always be set, but some non-standard
	// servers leave it NIL. Default to 7BIT.
	if bs.Encoding == "" {
		bs.Encoding = "7BIT"
	}

	// TODO: handle errors
	bs.Description, _ = options.decodeText(description)

	// Some servers don't include the extra fields for message and text
	// (see https://github.com/emersion/go-imap/issues/557)
	hasSP := dec.SP()
	if !hasSP {
		return &bs, nil
	}

	if strings.EqualFold(bs.Type, "message") && (strings.EqualFold(bs.Subtype, "rfc822") || strings.EqualFold(bs.Subtype, "global")) {
		var msg imap.BodyStructureMessageRFC822

		msg.Envelope, err = readEnvelope(dec, options)
		if err != nil {
			return nil, err
		}

		if !dec.ExpectSP() {
			return nil, dec.Err()
		}

		msg.BodyStructure, err = readBody(dec, options)
		if err != nil {
			return nil, err
		}

		if !dec.ExpectSP() || !dec.ExpectNumber64(&msg.NumLines) {
			return nil, dec.Err()
		}

		bs.MessageRFC822 = &msg
		hasSP = false
	} else if strings.EqualFold(bs.Type, "text") {
		var text imap.BodyStructureText

		if !dec.ExpectNumber64(&text.NumLines) {
			return nil, dec.Err()
		}

		bs.Text = &text
		hasSP = false
	}

	if !hasSP {
		hasSP = dec.SP()
	}
	if hasSP {
		bs.Extended, err = readBodyExt1part(dec, options)
		if err != nil {
			return nil, fmt.Errorf("in body-ext-1part: %v", err)
		}
	}

	return &bs, nil
}

func readBodyExt1part(dec *imapwire.Decoder, options *Options) (*imap.BodyStructureSinglePartExt, error) {
	var ext imap.BodyStructureSinglePartExt

	var md5 string
	if !dec.ExpectNString(&md5) {
		return nil, dec.Err()
	}

	if !dec.SP() {
		return &ext, nil
	}

	var err error
	ext.Disposition, err = readBodyFldDsp(dec, options)
	if err != nil {
		return nil, fmt.Errorf("in body-fld-dsp: %v", err)
	}

	if !dec.SP() {
		return &ext, nil
	}

	ext.Language, err = readBodyFldLang(dec)
	if err != nil {
		return nil, fmt.Errorf("in body-fld-lang: %v", err)
	}

	if !dec.SP() {
		return &ext, nil
	}

	if !dec.ExpectNString(&ext.Location) {
		return nil, dec.Err()
	}

	return &ext, nil
}

func readBodyTypeMpart(dec *imapwire.Decoder, options *Options) (*imap.BodyStructureMultiPart, error) {
	var bs imap.BodyStructureMultiPart

	for {
		child, err := readBody(dec, options)
		if err != nil {
			return nil, err
		}
		bs.Children = append(bs.Children, child)

		if dec.SP() && dec.String(&bs.Subtype) {
			break
		}
	}

	if dec.SP() {
		var err error
		bs.Extended, err = readBodyExtMpart(dec, options)
		if err != nil {
			return nil, fmt.Errorf("in body-ext-mpart: %v", err)
		}
	}

	return &bs, nil
}

func readBodyExtMpart(dec *imapwire.Decoder, options *Options) (*imap.BodyStructureMultiPartExt, error) {
	var ext imap.BodyStructureMultiPartExt

	var err error
	ext.Params, err = readBodyFldParam(dec, options)
	if err != nil {
		return nil, fmt.Errorf("in body-fld-param: %v", err)
	}

	if !dec.SP() {
		return &ext, nil
	}

	ext.Disposition, err = readBodyFldDsp(dec, options)
	if err != nil {
		return nil, fmt.Errorf("in body-fld-dsp: %v", err)
	}

	if !dec.SP() {
		return &ext, nil
	}

	ext.Language, err = readBodyFldLang(dec)
	if err != nil {
		return nil, fmt.Errorf("in body-fld-lang: %v", err)
	}

	if !dec.SP() {
		return &ext, nil
	}

	if !dec.ExpectNString(&ext.Location) {
		return nil, dec.Err()
	}

	return &ext, nil
}

func readBodyFldDsp(dec *imapwire.Decoder, options *Options) (*imap.BodyStructureDisposition, error) {
	if !dec.Special('(') {
		if !dec.ExpectNIL() {
			return nil, dec.Err()
		}
		return nil, nil
	}

	var disp imap.BodyStructureDisposition
	if !dec.ExpectString(&disp.Value) || !dec.ExpectSP() {
		return nil, dec.Err()
	}

	var err error
	disp.Params, err = readBodyFldParam(dec, options)
	if err != nil {
		return nil, err
	}
	if !dec.ExpectSpecial(')') {
		return nil, dec.Err()
	}
	return &disp, nil
}

func readBodyFldParam(dec *imapwire.Decoder, options *Options) (map[string]string, error) {
	var (
		params map[string]string
		k      string
	)
	err := dec.ExpectNList(func() error {
		var s string
		if !dec.ExpectString(&s) {
			return dec.Err()
		}

		if k == "" {
			k = s
		} else {
			if params == nil {
				params = make(map[string]string)
			}
			decoded, _ := options.decodeText(s)
			// TODO: handle error

			params[strings.ToLower(k)] = decoded
			k = ""
		}

		return nil
	})
	if err != nil {
		return nil, err
	} else if k != "" {
		return nil, fmt.Errorf("in body-fld-param: key without value")
	}
	return params, nil
}

func readBodyFldLang(dec *imapwire.Decoder) ([]string, error) {
	var l []string
	isList, err := dec.List(func() error {
		var s string
		if !dec.ExpectString(&s) {
			return dec.Err()
		}
		l = append(l, s)
		return nil
	})
	if err != nil || isList {
		return l, err
	}

	var s string
	if !dec.ExpectNString(&s) {
		return nil, dec.Err()
	}
	if s != "" {
		return []string{s}, nil
	} else {
		return nil, nil
	}
}

func readSectionSpec(dec *imapwire.Decoder) (*imap.FetchItemBodySection, error) {
	var section imap.FetchItemBodySection

	var dot bool
	section.Part, dot = readSectionPart(dec)
	if dot || len(section.Part) == 0 {
		var specifier string
		if dot {
			if !dec.ExpectAtom(&specifier) {
				return nil, dec.Err()
			}
		} else {
			dec.Atom(&specifier)
		}
		specifier = strings.ToUpper(specifier)
		section.Specifier = imap.PartSpecifier(specifier)

		if specifier == "HEADER.FIELDS" || specifier == "HEADER.FIELDS.NOT" {
			if !dec.ExpectSP() {
				return nil, dec.Err()
			}
			var err error
			headerList, err := readHeaderList(dec)
			if err != nil {
				return nil, err
			}
			section.Specifier = imap.PartSpecifierHeader
			if specifier == "HEADER.FIELDS" {
				section.HeaderFields = headerList
			} else {
				section.HeaderFieldsNot = headerList
			}
		}
	}

	if !dec.ExpectSpecial(']') {
		return nil, dec.Err()
	}

	offset, err := readPartialOffset(dec)
	if err != nil {
		return nil, err
	}
	if offset != nil {
		section.Partial = &imap.SectionPartial{Offset: int64(*offset)}
	}

	return &section, nil
}

func readPartialOffset(dec *imapwire.Decoder) (*uint32, error) {
	if !dec.Special('<') {
		return nil, nil
	}
	var offset uint32
	if !dec.ExpectNumber(&offset) || !dec.ExpectSpecial('>') {
		return nil, dec.Err()
	}
	return &offset, nil
}

func readHeaderList(dec *imapwire.Decoder) ([]string, error) {
	var l []string
	err := dec.ExpectList(func() error {
		var s string
		if !dec.ExpectAString(&s) {
			return dec.Err()
		}
		l = append(l, s)
		return nil
	})
	return l, err
}

func readSectionPart(dec *imapwire.Decoder) (part []int, dot bool) {
	for {
		dot = len(part) > 0
		if dot && !dec.Special('.') {
			return part, false
		}

		var num uint32
		if !dec.Number(&num) {
			return part, dot
		}
		part = append(part, int(num))
	}
}

type fetchLiteralReader struct {
	*imapwire.LiteralReader
	ch chan<- struct{}
}

func (lit *fetchLiteralReader) Read(b []byte) (int, error) {
	n, err := lit.LiteralReader.Read(b)
	if err != nil && lit.ch != nil {
		close(lit.ch)
		lit.ch = nil
	}
	return n, err
}
