package spartan

import (
	"bufio"
	"bytes"
	"errors"
	"io"
	"strconv"
	"sync"

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

// The spartan response types.
const (
	StatusSuccess     types.Status = 2
	StatusRedirect    types.Status = 3
	StatusClientError types.Status = 4
	StatusServerError types.Status = 5
)

// Success builds a successful spartan response.
func Success(mediatype string, body io.Reader) *types.Response {
	return &types.Response{
		Status: StatusSuccess,
		Meta:   mediatype,
		Body:   body,
	}
}

// Redirect builds a spartan redirect response.
func Redirect(url string) *types.Response {
	return &types.Response{
		Status: StatusRedirect,
		Meta:   url,
	}
}

// ClientError builds a "client error" spartan response.
func ClientError(err error) *types.Response {
	return &types.Response{
		Status: StatusClientError,
		Meta:   err.Error(),
	}
}

// ServerError builds a "server error" spartan response.
func ServerError(err error) *types.Response {
	return &types.Response{
		Status: StatusServerError,
		Meta:   err.Error(),
	}
}

// InvalidResponseHeaderLine indicates a malformed spartan response line.
var InvalidResponseHeaderLine = errors.New("Invalid response header line.")

// InvalidResponseLineEnding indicates that a spartan response header didn't end with "\r\n".
var InvalidResponseLineEnding = errors.New("Invalid response line ending.")

func ParseResponse(rdr io.Reader) (*types.Response, error) {
	bufrdr := bufio.NewReader(rdr)

	hdrLine, err := bufrdr.ReadString('\n')
	if err != nil {
		return nil, InvalidResponseLineEnding
	}
	if len(hdrLine) < 2 {
		return nil, InvalidResponseHeaderLine
	}

	status, err := strconv.Atoi(string(hdrLine[0]))
	if err != nil || hdrLine[1] != ' ' || hdrLine[len(hdrLine)-2:] != "\r\n" {
		return nil, InvalidResponseHeaderLine
	}

	return &types.Response{
		Status: types.Status(status),
		Meta:   hdrLine[2 : len(hdrLine)-2],
		Body:   bufrdr,
	}, nil
}

// NewResponseReader builds a reader for a response.
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() {
		hdr := bytes.NewBuffer(rdr.headerLine())
		if rdr.Body != nil {
			rdr.reader = io.MultiReader(hdr, rdr.Body)
		} else {
			rdr.reader = hdr
		}
	})
}

func (rdr *responseReader) headerLine() []byte {
	meta := rdr.Meta.(string)
	buf := make([]byte, len(meta)+4)
	buf[0] = byte(rdr.Status) + '0'
	buf[1] = ' '
	copy(buf[2:], meta)
	buf[len(buf)-2] = '\r'
	buf[len(buf)-1] = '\n'
	return buf
}