package mail

import (
	"bytes"
	"encoding/base64"
	"io"
	"mime/multipart"
	"mime/quotedprintable"
	"net/textproto"
	"regexp"
	"strconv"
	"strings"
	"time"
)

type message struct {
	headers  textproto.MIMEHeader
	body     *bytes.Buffer
	writers  []*multipart.Writer
	parts    uint8
	cids     map[string]string
	charset  string
	encoding encoding
}

func newMessage(email *Email) *message {
	return &message{
		headers:  email.headers,
		body:     new(bytes.Buffer),
		cids:     make(map[string]string),
		charset:  email.Charset,
		encoding: email.Encoding}
}

func encodeHeader(text string, charset string, usedChars int) string {
	// create buffer
	buf := new(bytes.Buffer)

	// encode
	encoder := newEncoder(buf, charset, usedChars)
	encoder.encode([]byte(text))

	return buf.String()

	/*
			switch encoding {
			case EncodingBase64:
				return mime.BEncoding.Encode(charset, text)
			default:
				return mime.QEncoding.Encode(charset, text)
		}
	*/
}

// getHeaders returns the message headers
func (msg *message) getHeaders() (headers string) {
	// if the date header isn't set, set it
	if date := msg.headers.Get("Date"); date == "" {
		msg.headers.Set("Date", time.Now().Format(time.RFC1123Z))
	}

	// encode and combine the headers
	for header, values := range msg.headers {
		headers += header + ": " + encodeHeader(strings.Join(values, ", "), msg.charset, len(header)+2) + "\r\n"
	}

	headers = headers + "\r\n"

	return
}

// getCID gets the generated CID for the provided text
func (msg *message) getCID(text string) (cid string) {
	// set the date format to use
	const dateFormat = "20060102.150405"

	// get the cid if we have one
	cid, exists := msg.cids[text]
	if !exists {
		// generate a new cid
		cid = time.Now().Format(dateFormat) + "." + strconv.Itoa(len(msg.cids)+1) + "@mail.0"
		// save it
		msg.cids[text] = cid
	}

	return
}

// replaceCIDs replaces the CIDs found in a text string
// with generated ones
func (msg *message) replaceCIDs(text string) string {
	// regular expression to find cids
	re := regexp.MustCompile(`(src|href)="cid:(.*?)"`)
	// replace all of the found cids with generated ones
	for _, matches := range re.FindAllStringSubmatch(text, -1) {
		cid := msg.getCID(matches[2])
		text = strings.Replace(text, "cid:"+matches[2], "cid:"+cid, -1)
	}

	return text
}

// openMultipart creates a new part of a multipart message
func (msg *message) openMultipart(multipartType string) {
	// create a new multipart writer
	msg.writers = append(msg.writers, multipart.NewWriter(msg.body))
	// create the boundary
	contentType := "multipart/" + multipartType + ";\n \tboundary=" + msg.writers[msg.parts].Boundary()

	// if no existing parts, add header to main header group
	if msg.parts == 0 {
		msg.headers.Set("Content-Type", contentType)
	} else { // add header to multipart section
		header := make(textproto.MIMEHeader)
		header.Set("Content-Type", contentType)
		msg.writers[msg.parts-1].CreatePart(header)
	}

	msg.parts++
}

// closeMultipart closes a part of a multipart message
func (msg *message) closeMultipart() {
	if msg.parts > 0 {
		msg.writers[msg.parts-1].Close()
		msg.parts--
	}
}

// base64Encode base64 encodes the provided text with line wrapping
func base64Encode(text []byte) []byte {
	// create buffer
	buf := new(bytes.Buffer)

	// create base64 encoder that linewraps
	encoder := base64.NewEncoder(base64.StdEncoding, &base64LineWrap{writer: buf})

	// write the encoded text to buf
	encoder.Write(text)
	encoder.Close()

	return buf.Bytes()
}

// qpEncode uses the quoted-printable encoding to encode the provided text
func qpEncode(text []byte) []byte {
	// create buffer
	buf := new(bytes.Buffer)

	encoder := quotedprintable.NewWriter(buf)

	encoder.Write(text)
	encoder.Close()

	return buf.Bytes()
}

const maxLineChars = 76

type base64LineWrap struct {
	writer       io.Writer
	numLineChars int
}

func (e *base64LineWrap) Write(p []byte) (n int, err error) {
	n = 0
	// while we have more chars than are allowed
	for len(p)+e.numLineChars > maxLineChars {
		numCharsToWrite := maxLineChars - e.numLineChars
		// write the chars we can
		e.writer.Write(p[:numCharsToWrite])
		// write a line break
		e.writer.Write([]byte("\r\n"))
		// reset the line count
		e.numLineChars = 0
		// remove the chars that have been written
		p = p[numCharsToWrite:]
		// set the num of chars written
		n += numCharsToWrite
	}

	// write what is left
	e.writer.Write(p)
	e.numLineChars += len(p)
	n += len(p)

	return
}

func (msg *message) write(header textproto.MIMEHeader, body []byte, encoding encoding) {
	msg.writeHeader(header)
	msg.writeBody(body, encoding)
}

func (msg *message) writeHeader(headers textproto.MIMEHeader) {
	// if there are no parts add header to main headers
	if msg.parts == 0 {
		for header, value := range headers {
			msg.headers[header] = value
		}
	} else { // add header to multipart section
		msg.writers[msg.parts-1].CreatePart(headers)
	}
}

func (msg *message) writeBody(body []byte, encoding encoding) {
	// encode and write the body
	switch encoding {
	case EncodingQuotedPrintable:
		msg.body.Write(qpEncode(body))
	case EncodingBase64:
		msg.body.Write(base64Encode(body))
	default:
		msg.body.Write(body)
	}
}

func (msg *message) addBody(contentType string, body []byte) {
	body = []byte(msg.replaceCIDs(string(body)))

	header := make(textproto.MIMEHeader)
	header.Set("Content-Type", contentType+"; charset="+msg.charset)
	header.Set("Content-Transfer-Encoding", msg.encoding.string())
	msg.write(header, body, msg.encoding)
}

var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")

func escapeQuotes(s string) string {
	return quoteEscaper.Replace(s)
}

func (msg *message) addFiles(files []*file, inline bool) {
	encoding := EncodingBase64
	for _, file := range files {
		header := make(textproto.MIMEHeader)
		header.Set("Content-Type", file.mimeType+";\n \tname=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 6)+`"`)
		header.Set("Content-Transfer-Encoding", encoding.string())
		if inline {
			header.Set("Content-Disposition", "inline;\n \tfilename=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 10)+`"`)
			header.Set("Content-ID", "<"+msg.getCID(file.filename)+">")
		} else {
			header.Set("Content-Disposition", "attachment;\n \tfilename=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 10)+`"`)
		}

		msg.write(header, file.data, encoding)
	}
}
