package commands

import (
	"bytes"
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"os"
	"strings"

	cmds "gx/ipfs/QmZ9hww8R3FKrDRCYPxhN13m6XgjPDpaSvdUfisPvERzXz/go-ipfs-cmds"
	cmdkit "gx/ipfs/QmceUdzxkimdYsgtX733uNgzf1DLHyBKN6ehGSp85ayppM/go-ipfs-cmdkit"
)

// ErrorType signfies a category of errors
type ErrorType uint

// EncodingType defines a supported encoding
type EncodingType string

// Supported EncodingType constants.
const (
	JSON     = "json"
	XML      = "xml"
	Protobuf = "protobuf"
	Text     = "text"
	// TODO: support more encoding types
)

func marshalJson(value interface{}) (io.Reader, error) {
	b, err := json.Marshal(value)
	if err != nil {
		return nil, err
	}
	b = append(b, '\n')
	return bytes.NewReader(b), nil
}

var marshallers = map[EncodingType]Marshaler{
	JSON: func(res Response) (io.Reader, error) {
		ch, ok := res.Output().(<-chan interface{})
		if ok {
			return &ChannelMarshaler{
				Channel:   ch,
				Marshaler: marshalJson,
				Res:       res,
			}, nil
		}

		var value interface{}
		if res.Error() != nil {
			value = res.Error()
		} else {
			value = res.Output()
		}
		return marshalJson(value)
	},
	XML: func(res Response) (io.Reader, error) {
		var value interface{}
		if res.Error() != nil {
			value = res.Error()
		} else {
			value = res.Output()
		}

		b, err := xml.Marshal(value)
		if err != nil {
			return nil, err
		}
		return bytes.NewReader(b), nil
	},
}

// Response is the result of a command request. Handlers write to the response,
// setting Error or Value. Response is returned to the client.
type Response interface {
	Request() Request

	// Set/Return the response Error
	SetError(err error, code cmdkit.ErrorType)
	Error() *cmdkit.Error

	// Sets/Returns the response value
	SetOutput(interface{})
	Output() interface{}

	// Sets/Returns the length of the output
	SetLength(uint64)
	Length() uint64

	// underlying http connections need to be cleaned up, this is for that
	Close() error
	SetCloser(io.Closer)

	// Marshal marshals out the response into a buffer. It uses the EncodingType
	// on the Request to chose a Marshaler (Codec).
	Marshal() (io.Reader, error)

	// Gets a io.Reader that reads the marshalled output
	Reader() (io.Reader, error)

	// Gets Stdout and Stderr, for writing to console without using SetOutput
	Stdout() io.Writer
	Stderr() io.Writer
}

type response struct {
	req    Request
	err    *cmdkit.Error
	value  interface{}
	out    io.Reader
	length uint64
	stdout io.Writer
	stderr io.Writer
	closer io.Closer
}

func (r *response) Request() Request {
	return r.req
}

func (r *response) Output() interface{} {
	return r.value
}

func (r *response) SetOutput(v interface{}) {
	r.value = v
}

func (r *response) Length() uint64 {
	return r.length
}

func (r *response) SetLength(l uint64) {
	r.length = l
}

func (r *response) Error() *cmdkit.Error {
	return r.err
}

func (r *response) SetError(err error, code cmdkit.ErrorType) {
	r.err = &cmdkit.Error{Message: err.Error(), Code: code}
}

func (r *response) Marshal() (io.Reader, error) {
	if r.err == nil && r.value == nil {
		return bytes.NewReader([]byte{}), nil
	}

	enc, found, err := r.req.Option(cmds.EncLong).String()
	if err != nil {
		return nil, err
	}
	if !found {
		return nil, fmt.Errorf("No encoding type was specified")
	}
	encType := EncodingType(strings.ToLower(enc))

	// Special case: if text encoding and an error, just print it out.
	if encType == Text && r.Error() != nil {
		return strings.NewReader(r.Error().Error()), nil
	}

	var marshaller Marshaler
	if r.req.Command() != nil && r.req.Command().Marshalers != nil {
		marshaller = r.req.Command().Marshalers[encType]
	}
	if marshaller == nil {
		var ok bool
		marshaller, ok = marshallers[encType]
		if !ok {
			return nil, fmt.Errorf("No marshaller found for encoding type '%s'", enc)
		}
	}

	output, err := marshaller(r)
	if err != nil {
		return nil, err
	}
	if output == nil {
		return bytes.NewReader([]byte{}), nil
	}
	return output, nil
}

// Reader returns an `io.Reader` representing marshalled output of this Response
// Note that multiple calls to this will return a reference to the same io.Reader
func (r *response) Reader() (io.Reader, error) {
	if r.out == nil {
		if out, ok := r.value.(io.Reader); ok {
			// if command returned a io.Reader, use that as our reader
			r.out = out

		} else {
			// otherwise, use the response marshaler output
			marshalled, err := r.Marshal()
			if err != nil {
				return nil, err
			}

			r.out = marshalled
		}
	}

	return r.out, nil
}

func (r *response) Close() error {
	if r.closer != nil {
		return r.closer.Close()
	}
	return nil
}

func (r *response) SetCloser(c io.Closer) {
	r.closer = c
}

func (r *response) Stdout() io.Writer {
	return r.stdout
}

func (r *response) Stderr() io.Writer {
	return r.stderr
}

// NewResponse returns a response to match given Request
func NewResponse(req Request) Response {
	return &response{
		req:    req,
		stdout: os.Stdout,
		stderr: os.Stderr,
	}
}
