//
// attachment.go -- nntp attachements
//

package srnd

import (
	"bytes"
	"crypto/sha512"
	"encoding/base32"
	"encoding/base64"
	"errors"
	"io"
	"log"
	"mime"
	"mime/multipart"
	"net/textproto"
	"os"
	"path/filepath"
	"strings"
)

type NNTPAttachment interface {
	io.WriterTo
	io.Writer
	// the name of the file
	Filename() string
	// the filepath to the saved file
	Filepath() string
	// the mime type of the attachment
	Mime() string
	// the file extension of the attachment
	Extension() string
	// get the sha512 hash of the attachment
	Hash() []byte
	// do we need to generate a thumbnail?
	NeedsThumbnail() bool
	// mime header
	Header() textproto.MIMEHeader
	// make into a model
	ToModel(prefix string) AttachmentModel
	// base64'd file data
	Filedata() string
	// as raw string
	AsString() string
	// reset contents
	Reset()
	// get bytes
	Bytes() []byte
	// save to directory, filename is decided by the attachment
	Save(dir string) error
	// get body as io.ReadCloser
	OpenBody() (io.ReadCloser, error)
}

type nntpAttachment struct {
	ext      string
	mime     string
	filename string
	filepath string
	hash     []byte
	header   textproto.MIMEHeader
	body     *bytes.Buffer
	rawpath  string
	store    ArticleStore
}

type byteBufferReadCloser struct {
	b *bytes.Buffer
}

func (b *byteBufferReadCloser) Close() error {
	b.b.Reset()
	return nil
}

func (b *byteBufferReadCloser) Read(d []byte) (int, error) {
	return b.b.Read(d)
}

func (self *nntpAttachment) OpenBody() (io.ReadCloser, error) {
	if self.store != nil {
		return os.Open(self.store.AttachmentFilepath(self.filepath))
	} else {
		return &byteBufferReadCloser{
			self.body,
		}, nil
	}
}

func (self *nntpAttachment) Reset() {
	self.body = nil
	self.header = nil
	self.hash = nil
	self.filepath = ""
	self.filename = ""
	self.mime = ""
	self.ext = ""
	self.store = nil
}

func (self *nntpAttachment) ToModel(prefix string) AttachmentModel {
	return &attachment{
		prefix: prefix,
		Path:   self.Filepath(),
		Name:   self.Filename(),
	}
}

func (self *nntpAttachment) Bytes() []byte {
	if self.body == nil {
		return nil
	}
	return self.body.Bytes()
}

func (self *nntpAttachment) Save(dir string) (err error) {
	if self.body == nil {
		// no body wat
		err = errors.New("no attachment body")
	} else {
		fpath := filepath.Join(dir, self.filepath)
		if !CheckFile(fpath) {
			var f io.WriteCloser
			// does not exist so will will write it
			f, err = os.Create(fpath)
			if err == nil {
				_, err = f.Write(self.Bytes())
				f.Close()
			}
		}
	}
	return
}

func (self *nntpAttachment) Write(b []byte) (int, error) {
	if self.body == nil {
		self.body = new(bytes.Buffer)
	}
	return self.body.Write(b)
}

func (self *nntpAttachment) AsString() string {
	if self.body == nil {
		return ""
	}
	return string(self.Bytes())
}

func (self *nntpAttachment) Filedata() string {
	e := base64.StdEncoding
	str := e.EncodeToString(self.Bytes())
	e = nil
	return str
}

func (self *nntpAttachment) Filename() string {
	return self.filename
}

func (self *nntpAttachment) Filepath() string {
	return self.filepath
}

func (self *nntpAttachment) Mime() string {
	return self.mime
}

func (self *nntpAttachment) Extension() string {
	return self.ext
}

func (self *nntpAttachment) WriteTo(wr io.Writer) (int64, error) {
	w, err := wr.Write(self.Bytes())
	return int64(w), err
}

func (self *nntpAttachment) Hash() []byte {
	// hash it if we haven't already
	if self.hash == nil || len(self.hash) == 0 {
		h := sha512.Sum512(self.Bytes())
		self.hash = h[:]
	}
	return self.hash
}

// TODO: detect
func (self *nntpAttachment) NeedsThumbnail() bool {
	for _, ext := range []string{".png", ".jpeg", ".jpg", ".gif", ".bmp", ".webm", ".mp4", ".avi", ".mpeg", ".mpg", ".ogg", ".mp3", ".oga", ".opus", ".flac", ".ico", "m4a"} {
		if ext == strings.ToLower(self.ext) {
			return true
		}
	}
	return false
}

func (self *nntpAttachment) Header() textproto.MIMEHeader {
	return self.header
}

// create a plaintext attachment
func createPlaintextAttachment(msg []byte) NNTPAttachment {
	header := make(textproto.MIMEHeader)
	mime := "text/plain; charset=UTF-8"
	header.Set("Content-Type", mime)
	header.Set("Content-Transfer-Encoding", "base64")
	att := &nntpAttachment{
		mime:   mime,
		ext:    ".txt",
		header: header,
	}
	msg = bytes.Trim(msg, "\r")
	att.Write(msg)
	return att
}

// assumes base64'd
func createAttachment(content_type, fname string, body io.Reader) NNTPAttachment {

	media_type, _, err := mime.ParseMediaType(content_type)
	if err == nil {
		a := new(nntpAttachment)
		dec := base64.NewDecoder(base64.StdEncoding, body)
		_, err = io.Copy(a, dec)
		if err == nil {
			a.header = make(textproto.MIMEHeader)
			a.mime = media_type + "; charset=UTF-8"
			idx := strings.LastIndex(fname, ".")
			a.ext = ".txt"
			if idx > 0 {
				a.ext = fname[idx:]
			}
			a.header.Set("Content-Disposition", `form-data; filename="`+fname+`"; name="attachment"`)
			a.header.Set("Content-Type", a.mime)
			a.header.Set("Content-Transfer-Encoding", "base64")
			h := a.Hash()
			hashstr := base32.StdEncoding.EncodeToString(h[:])
			a.hash = h[:]
			a.filepath = hashstr + a.ext
			a.filename = fname
			return a
		}
	}
	return nil
}

func readAttachmentFromMimePartAndStore(part *multipart.Part, store ArticleStore) NNTPAttachment {
	hdr := part.Header
	att := &nntpAttachment{}
	att.store = store
	att.header = hdr
	content_type := hdr.Get("Content-Type")
	var err error
	att.mime, _, err = mime.ParseMediaType(content_type)
	att.filename = part.FileName()
	idx := strings.LastIndex(att.filename, ".")
	att.ext = ".txt"
	if idx > 0 {
		att.ext = att.filename[idx:]
	}
	h := sha512.New()
	transfer_encoding := hdr.Get("Content-Transfer-Encoding")
	var r io.Reader
	if transfer_encoding == "base64" {
		// decode
		r = base64.NewDecoder(base64.StdEncoding, part)
	} else {
		r = part
	}
	var fpath string
	var mw io.Writer
	if store == nil {
		mw = io.MultiWriter(att, h)
	} else {
		fname := randStr(10) + ".temp"
		fpath = filepath.Join(store.AttachmentDir(), fname)
		f, err := os.Create(fpath)
		if err != nil {
			log.Println("!!! failed to store attachment: ", err, "!!!")
			return nil
		}
		defer f.Close()
		if strings.ToLower(att.mime) == "text/plain" {
			mw = io.MultiWriter(f, h, att)
		} else {
			mw = io.MultiWriter(f, h)
		}
	}
	_, err = io.Copy(mw, r)
	if err != nil {
		log.Println("failed to read attachment from mimepart", err)
		if fpath != "" {
			DelFile(fpath)
		}
		return nil
	}
	hsh := h.Sum(nil)
	att.hash = hsh[:]
	enc := base32.StdEncoding
	hashstr := enc.EncodeToString(att.hash[:])
	att.filepath = hashstr + att.ext
	// we are good just return it
	if store == nil {
		return att
	}
	att_fpath := filepath.Join(store.AttachmentDir(), att.filepath)
	if !CheckFile(att_fpath) {
		// attachment isn't there
		// move it into it
		err = os.Rename(fpath, att_fpath)
	} else {
		DelFile(fpath)
	}
	if err == nil {
		// now thumbnail
		if !CheckFile(store.ThumbnailFilepath(att.filepath)) {
			store.GenerateThumbnail(att.filepath)
		}
	} else {
		// wtf?
		log.Println("!!! failed to store attachment", err, "!!!")
		DelFile(fpath)
	}
	return att
}
