// Package web is the entry point for the backend of the web interface of
// Elvish.
package web

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"

	"src.elv.sh/pkg/eval"
	"src.elv.sh/pkg/parse"
	"src.elv.sh/pkg/prog"
	"src.elv.sh/pkg/shell"
)

// Program is the web subprogram.
var Program prog.Program = program{}

type program struct{}

func (program) ShouldRun(f *prog.Flags) bool { return f.Web }

func (program) Run(fds [3]*os.File, f *prog.Flags, args []string) error {
	if len(args) > 0 {
		return prog.BadUsage("arguments are not allowed with -web")
	}
	if f.CodeInArg {
		return prog.BadUsage("-c cannot be used together with -web")
	}
	p := Web{Port: f.Port}
	return p.Main(fds, nil)
}

type Web struct {
	Port int
}

type httpHandler struct {
	ev *eval.Evaler
}

type ExecuteResponse struct {
	OutBytes  string
	OutValues []interface{}
	ErrBytes  string
	Err       string
}

func (web *Web) Main(fds [3]*os.File, _ []string) error {
	restore := shell.IncSHLVL()
	defer restore()
	ev := shell.MakeEvaler(fds[2])

	h := httpHandler{ev}

	http.HandleFunc("/", h.handleMainPage)
	http.HandleFunc("/execute", h.handleExecute)
	addr := fmt.Sprintf("localhost:%d", web.Port)
	log.Println("going to listen", addr)
	err := http.ListenAndServe(addr, nil)

	log.Println(err)
	return nil
}

func (h httpHandler) handleMainPage(w http.ResponseWriter, r *http.Request) {
	_, err := w.Write([]byte(mainPageHTML))
	if err != nil {
		log.Println("cannot write response:", err)
	}
}

func (h httpHandler) handleExecute(w http.ResponseWriter, r *http.Request) {
	bytes, err := ioutil.ReadAll(r.Body)
	if err != nil {
		log.Println("cannot read request body:", err)
		return
	}
	code := string(bytes)

	outBytes, outValues, errBytes, err := evalAndCollect(h.ev, code)
	errText := ""
	if err != nil {
		errText = err.Error()
	}
	responseBody, err := json.Marshal(
		&ExecuteResponse{string(outBytes), outValues, string(errBytes), errText})
	if err != nil {
		log.Println("cannot marshal response body:", err)
	}

	_, err = w.Write(responseBody)
	if err != nil {
		log.Println("cannot write response:", err)
	}
}

const (
	outFileBufferSize = 1024
	outChanBufferSize = 32
)

// evalAndCollect evaluates a piece of code with null stdin, and stdout and
// stderr connected to pipes (value part of stderr being a blackhole), and
// return the results collected on stdout and stderr, and the possible error
// that occurred.
func evalAndCollect(ev *eval.Evaler, code string) (
	outBytes []byte, outValues []interface{}, errBytes []byte, err error) {

	outFile, chanOutBytes := makeBytesWriterAndCollect()
	outChan, chanOutValues := makeValuesWriterAndCollect()
	errFile, chanErrBytes := makeBytesWriterAndCollect()

	ports := []*eval.Port{
		eval.DummyInputPort,
		{File: outFile, Chan: outChan},
		{File: errFile, Chan: eval.BlackholeChan},
	}
	err = ev.Eval(
		parse.Source{Name: "[web]", Code: code}, eval.EvalCfg{Ports: ports})

	outFile.Close()
	close(outChan)
	errFile.Close()
	return <-chanOutBytes, <-chanOutValues, <-chanErrBytes, err
}

// makeBytesWriterAndCollect makes an in-memory file that can be written to, and
// the written bytes will be collected in a byte slice that will be put on a
// channel as soon as the writer is closed.
func makeBytesWriterAndCollect() (*os.File, <-chan []byte) {
	r, w, err := os.Pipe()
	// os.Pipe returns error only on resource exhaustion.
	if err != nil {
		panic(err)
	}
	chanCollected := make(chan []byte)

	go func() {
		var (
			collected []byte
			buf       [outFileBufferSize]byte
		)
		for {
			n, err := r.Read(buf[:])
			collected = append(collected, buf[:n]...)
			if err != nil {
				if err != io.EOF {
					log.Println("error when reading output pipe:", err)
				}
				break
			}
		}
		r.Close()
		chanCollected <- collected
	}()

	return w, chanCollected
}

// makeValuesWriterAndCollect makes a Value channel for writing, and the written
// values will be collected in a Value slice that will be put on a channel as
// soon as the writer is closed.
func makeValuesWriterAndCollect() (chan interface{}, <-chan []interface{}) {
	chanValues := make(chan interface{}, outChanBufferSize)
	chanCollected := make(chan []interface{})

	go func() {
		var collected []interface{}
		for {
			for v := range chanValues {
				collected = append(collected, v)
			}
			chanCollected <- collected
		}
	}()

	return chanValues, chanCollected
}
