// Package repl implements a REPL (read-eval-print loop) for otto.
package repl

import (
	"errors"
	"fmt"
	"io"
	"strings"
	"sync/atomic"

	"github.com/robertkrimen/otto"
	"gopkg.in/readline.v1"
)

var counter uint32

// DebuggerHandler implements otto's debugger handler signature, providing a
// simple drop-in debugger implementation.
func DebuggerHandler(vm *otto.Otto) {
	i := atomic.AddUint32(&counter, 1)

	// purposefully ignoring the error here - we can't do anything useful with
	// it except panicking, and that'd be pretty rude. it'd be easy enough for a
	// consumer to define an equivalent function that _does_ panic if desired.
	_ = RunWithPrompt(vm, fmt.Sprintf("DEBUGGER[%d]> ", i))
}

// Run creates a REPL with the default prompt and no prelude.
func Run(vm *otto.Otto) error {
	return RunWithOptions(vm, Options{})
}

// RunWithPrompt runs a REPL with the given prompt and no prelude.
func RunWithPrompt(vm *otto.Otto, prompt string) error {
	return RunWithOptions(vm, Options{
		Prompt: prompt,
	})
}

// RunWithPrelude runs a REPL with the default prompt and the given prelude.
func RunWithPrelude(vm *otto.Otto, prelude string) error {
	return RunWithOptions(vm, Options{
		Prelude: prelude,
	})
}

// RunWithPromptAndPrelude runs a REPL with the given prompt and prelude.
func RunWithPromptAndPrelude(vm *otto.Otto, prompt, prelude string) error {
	return RunWithOptions(vm, Options{
		Prompt:  prompt,
		Prelude: prelude,
	})
}

// Options contains parameters for configuring a REPL session.
type Options struct {
	// Prompt controls what's shown at the beginning of the line. If not
	// specified, this defaults to "> "
	Prompt string
	// Prelude is some text that's printed before control is given to the user.
	// By default this is empty. If specified, this will be printed with a
	// newline following it.
	Prelude string
	// Autocomplete controls whether this REPL session has autocompletion
	// enabled. The way autocomplete is implemented can incur a performance
	// penalty, so it's turned off by default.
	Autocomplete bool
}

// RunWithOptions runs a REPL with the given options.
func RunWithOptions(vm *otto.Otto, options Options) error {
	prompt := options.Prompt
	if prompt == "" {
		prompt = "> "
	}

	c := &readline.Config{
		Prompt: prompt,
	}

	if options.Autocomplete {
		c.AutoComplete = &autoCompleter{vm}
	}

	rl, err := readline.NewEx(c)
	if err != nil {
		return err
	}

	prelude := options.Prelude
	if prelude != "" {
		if _, err = io.Copy(rl.Stderr(), strings.NewReader(prelude+"\n")); err != nil {
			return err
		}

		rl.Refresh()
	}

	var d []string

	for {
		l, errRL := rl.Readline()
		if errRL != nil {
			if errors.Is(errRL, readline.ErrInterrupt) {
				if d != nil {
					d = nil

					rl.SetPrompt(prompt)
					rl.Refresh()

					continue
				}

				break
			}

			return errRL
		}

		if l == "" {
			continue
		}

		d = append(d, l)

		s, errRL := vm.Compile("repl", strings.Join(d, "\n"))
		if errRL != nil {
			rl.SetPrompt(strings.Repeat(" ", len(prompt)))
		} else {
			rl.SetPrompt(prompt)

			d = nil

			v, errEval := vm.Eval(s)
			if errEval != nil {
				var oerr *otto.Error
				if errors.As(errEval, &oerr) {
					if _, err = io.Copy(rl.Stdout(), strings.NewReader(oerr.String())); err != nil {
						return fmt.Errorf("write out: %w", err)
					}
				} else {
					if _, err = io.Copy(rl.Stdout(), strings.NewReader(errEval.Error())); err != nil {
						return fmt.Errorf("write out: %w", err)
					}
				}
			} else {
				if _, err = rl.Stdout().Write([]byte(v.String() + "\n")); err != nil {
					return fmt.Errorf("write out: %w", err)
				}
			}
		}

		rl.Refresh()
	}

	return rl.Close()
}
