package gopher

import (
	"bytes"
	"fmt"
	"io"
	"mime"
	"path"
	"strings"
	"sync"

	"tildegit.org/tjp/sliderule/internal"
	"tildegit.org/tjp/sliderule/internal/types"
)

// The Canonical gopher item types.
const (
	TextFileType      types.Status = '0'
	MenuType          types.Status = '1'
	CSOPhoneBookType  types.Status = '2'
	ErrorType         types.Status = '3'
	MacBinHexType     types.Status = '4'
	DosBinType        types.Status = '5'
	UuencodedType     types.Status = '6'
	SearchType        types.Status = '7'
	TelnetSessionType types.Status = '8'
	BinaryFileType    types.Status = '9'
	MirrorServerType  types.Status = '+'
	GifFileType       types.Status = 'g'
	ImageFileType     types.Status = 'I'
	Telnet3270Type    types.Status = 'T'
)

// The gopher+ types.
const (
	BitmapType    types.Status = ':'
	MovieFileType types.Status = ';'
	SoundFileType types.Status = '<'
)

// The various non-canonical gopher types.
const (
	DocumentType     types.Status = 'd'
	HTMLType         types.Status = 'h'
	InfoMessageType  types.Status = 'i'
	PngImageFileType types.Status = 'p'
	RtfDocumentType  types.Status = 'r'
	WavSoundFileType types.Status = 's'
	PdfDocumentType  types.Status = 'P'
	XmlDocumentType  types.Status = 'X'
)

// MapItem is a single item in a gophermap.
type MapItem struct {
	Type     types.Status
	Display  string
	Selector string
	Hostname string
	Port     string
}

// String serializes the item into a gophermap CRLF-terminated text line.
func (mi MapItem) String() string {
	return fmt.Sprintf(
		"%s%s\t%s\t%s\t%s\r\n",
		[]byte{byte(mi.Type)},
		mi.Display,
		mi.Selector,
		mi.Hostname,
		mi.Port,
	)
}

// Response builds a response which contains just this single MapItem.
//
// Meta in the response will be a pointer to the MapItem.
func (mi *MapItem) Response() *types.Response {
	return &types.Response{
		Status: mi.Type,
		Meta:   &mi,
		Body:   bytes.NewBufferString(mi.String() + ".\r\n"),
	}
}

// MapDocument is a list of map items which can print out a full gophermap document.
type MapDocument []MapItem

// String serializes the document into gophermap format.
func (md MapDocument) String() string {
	return md.serialize().String()
}

// Response builds a gopher response containing the gophermap.
//
// Meta will be the MapDocument itself.
func (md MapDocument) Response() *types.Response {
	return &types.Response{
		Status: MenuType,
		Meta:   md,
		Body:   md.serialize(),
	}
}

func (md MapDocument) serialize() *bytes.Buffer {
	buf := &bytes.Buffer{}
	for _, mi := range md {
		_, _ = buf.WriteString(mi.String())
	}
	_, _ = buf.WriteString(".\r\n")
	return buf
}

// Error builds an error message MapItem.
func Error(err error) *MapItem {
	return &MapItem{
		Type:     ErrorType,
		Display:  err.Error(),
		Hostname: "none",
		Port:     "0",
	}
}

// File builds a minimal response delivering a file's contents.
//
// Meta is nil in this response.
func File(status types.Status, contents io.Reader) *types.Response {
	return &types.Response{Status: status, Body: contents}
}

// NewResponseReader produces a reader which supports reading gopher protocol responses.
func NewResponseReader(response *types.Response) types.ResponseReader {
	return &responseReader{
		Response: response,
		once:     &sync.Once{},
	}
}

type responseReader struct {
	*types.Response
	reader io.Reader
	once   *sync.Once
}

func (rdr *responseReader) Read(b []byte) (int, error) {
	rdr.ensureReader()
	return rdr.reader.Read(b)
}

func (rdr *responseReader) WriteTo(dst io.Writer) (int64, error) {
	rdr.ensureReader()
	return rdr.reader.(io.WriterTo).WriteTo(dst)
}

func (rdr *responseReader) ensureReader() {
	rdr.once.Do(func() {
		if _, ok := rdr.Body.(io.WriterTo); ok {
			rdr.reader = rdr.Body
			return
		}

		// rdr.reader needs to implement WriterTo, so in this case
		// we borrow an implementation in terms of io.Reader from
		// io.MultiReader.
		rdr.reader = io.MultiReader(rdr.Body)
	})
}

// GuessItemType attempts to find the best gopher item type for a file based on its name.
func GuessItemType(filepath string) types.Status {
	ext := path.Ext(filepath)
	switch ext {
	case ".gophermap", ".gph":
		return MenuType
	case ".txt", ".gmi", ".md":
		return TextFileType
	case ".gif":
		return GifFileType
	case ".png":
		return PngImageFileType
	case ".jpg", ".jpeg", ".tif", ".tiff":
		return ImageFileType
	case ".mp4", ".mov":
		return MovieFileType
	case ".pcm", ".mp3", ".aiff", ".aif", ".aac", ".ogg", ".flac", ".alac", ".wma":
		return SoundFileType
	case ".bmp":
		return BitmapType
	case ".doc", ".docx", ".odt", ".fodt":
		return DocumentType
	case ".html", ".htm":
		return HTMLType
	case ".rtf":
		return RtfDocumentType
	case ".wav":
		return WavSoundFileType
	case ".pdf":
		return PdfDocumentType
	case ".xml", ".atom":
		return XmlDocumentType
	case ".exe", ".bin", ".out", ".dylib", ".dll", ".so", ".a", ".o":
		return BinaryFileType
	}

	mtype := mime.TypeByExtension(ext)
	if strings.HasPrefix(mtype, "text/") {
		return TextFileType
	}

	if internal.ContentsAreText(filepath) {
		return TextFileType
	}

	return BinaryFileType
}