package message

import (
	"errors"
	"fmt"
	"io"
	"strings"

	"github.com/emersion/go-message/textproto"
)

// Writer writes message entities.
//
// If the message is not multipart, it should be used as a WriteCloser. Don't
// forget to call Close.
//
// If the message is multipart, users can either use CreatePart to write child
// parts or Write to directly pipe a multipart message. In any case, Close must
// be called at the end.
type Writer struct {
	w  io.Writer
	c  io.Closer
	mw *textproto.MultipartWriter
}

// createWriter creates a new Writer writing to w with the provided header.
// Nothing is written to w when it is called. header is modified in-place.
func createWriter(w io.Writer, header *Header) (*Writer, error) {
	ww := &Writer{w: w}

	mediaType, mediaParams, _ := header.ContentType()
	if strings.HasPrefix(mediaType, "multipart/") {
		ww.mw = textproto.NewMultipartWriter(ww.w)

		// Do not set ww's io.Closer for now: if this is a multipart entity but
		// CreatePart is not used (only Write is used), then the final boundary
		// is expected to be written by the user too. In this case, ww.Close
		// shouldn't write the final boundary.

		if mediaParams["boundary"] != "" {
			ww.mw.SetBoundary(mediaParams["boundary"])
		} else {
			mediaParams["boundary"] = ww.mw.Boundary()
			header.SetContentType(mediaType, mediaParams)
		}

		header.Del("Content-Transfer-Encoding")
	} else {
		wc, err := encodingWriter(header.Get("Content-Transfer-Encoding"), ww.w)
		if err != nil {
			return nil, err
		}
		ww.w = wc
		ww.c = wc
	}

	switch strings.ToLower(mediaParams["charset"]) {
	case "", "us-ascii", "utf-8":
		// This is OK
	default:
		// Anything else is invalid
		return nil, fmt.Errorf("unhandled charset %q", mediaParams["charset"])
	}

	return ww, nil
}

// CreateWriter creates a new message writer to w. If header contains an
// encoding, data written to the Writer will automatically be encoded with it.
// The charset needs to be utf-8 or us-ascii.
func CreateWriter(w io.Writer, header Header) (*Writer, error) {
	// Ensure that modifications are invisible to the caller
	header = header.Copy()

	// If the message uses MIME, it has to include MIME-Version
	if !header.Has("Mime-Version") {
		header.Set("MIME-Version", "1.0")
	}

	ww, err := createWriter(w, &header)
	if err != nil {
		return nil, err
	}
	if err := textproto.WriteHeader(w, header.Header); err != nil {
		return nil, err
	}
	return ww, nil
}

// Write implements io.Writer.
func (w *Writer) Write(b []byte) (int, error) {
	return w.w.Write(b)
}

// Close implements io.Closer.
func (w *Writer) Close() error {
	if w.c != nil {
		return w.c.Close()
	}
	return nil
}

// CreatePart returns a Writer to a new part in this multipart entity. If this
// entity is not multipart, it fails. The body of the part should be written to
// the returned io.WriteCloser.
func (w *Writer) CreatePart(header Header) (*Writer, error) {
	if w.mw == nil {
		return nil, errors.New("cannot create a part in a non-multipart message")
	}

	if w.c == nil {
		// We know that the user calls CreatePart so Close should write the final
		// boundary
		w.c = w.mw
	}

	// cw -> ww -> pw -> w.mw -> w.w

	ww := &struct{ io.Writer }{nil}

	// ensure that modifications are invisible to the caller
	header = header.Copy()
	cw, err := createWriter(ww, &header)
	if err != nil {
		return nil, err
	}
	pw, err := w.mw.CreatePart(header.Header)
	if err != nil {
		return nil, err
	}

	ww.Writer = pw
	return cw, nil
}
