package terminal

import (
	"bufio"
	"io"
	"os"
	"os/exec"
	"strings"

	"github.com/go-delve/delve/pkg/terminal/colorize"
	"github.com/mattn/go-isatty"
)

// transcriptWriter writes to a pagingWriter and also, optionally, to a
// buffered file.
type transcriptWriter struct {
	fileOnly     bool
	pw           *pagingWriter
	file         *bufio.Writer
	fh           io.Closer
	colorEscapes map[colorize.Style]string
}

func (w *transcriptWriter) Write(p []byte) (nn int, err error) {
	if !w.fileOnly {
		nn, err = w.pw.Write(p)
	}
	if err == nil {
		if w.file != nil {
			return w.file.Write(p)
		}
	}
	return
}

// ColorizePrint prints to out a syntax highlighted version of the text read from
// reader, between lines startLine and endLine.
func (w *transcriptWriter) ColorizePrint(path string, reader io.ReadSeeker, startLine, endLine, arrowLine int) error {
	var err error
	if !w.fileOnly {
		err = colorize.Print(w.pw.w, path, reader, startLine, endLine, arrowLine, w.colorEscapes)
	}
	if err == nil {
		if w.file != nil {
			reader.Seek(0, io.SeekStart)
			return colorize.Print(w.file, path, reader, startLine, endLine, arrowLine, nil)
		}
	}
	return err
}

// Echo outputs str only to the optional transcript file.
func (w *transcriptWriter) Echo(str string) {
	if w.file != nil {
		w.file.WriteString(str)
	}
}

// Flush flushes the optional transcript file.
func (w *transcriptWriter) Flush() {
	if w.file != nil {
		w.file.Flush()
	}
}

// CloseTranscript closes the optional transcript file.
func (w *transcriptWriter) CloseTranscript() error {
	if w.file == nil {
		return nil
	}
	w.file.Flush()
	w.fileOnly = false
	err := w.fh.Close()
	w.file = nil
	w.fh = nil
	return err
}

// TranscribeTo starts transcribing the output to the specified file. If
// fileOnly is true the output will only go to the file, output to the
// io.Writer will be suppressed.
func (w *transcriptWriter) TranscribeTo(fh io.WriteCloser, fileOnly bool) {
	if w.file == nil {
		w.CloseTranscript()
	}
	w.fh = fh
	w.file = bufio.NewWriter(fh)
	w.fileOnly = fileOnly
}

// pagingWriter writes to w. If PageMaybe is called, after a large amount of
// text has been written to w it will pipe the output to a pager instead.
type pagingWriter struct {
	mode     pagingWriterMode
	w        io.Writer
	buf      []byte
	cmd      *exec.Cmd
	cmdStdin io.WriteCloser
	pager    string
	lastnl   bool
	cancel   func()
}

type pagingWriterMode uint8

const (
	pagingWriterNormal pagingWriterMode = iota
	pagingWriterMaybe
	pagingWriterPaging

	pagingWriterMaxLines    = 30
	pagingWriterColsPerLine = 100
)

func (w *pagingWriter) Write(p []byte) (nn int, err error) {
	switch w.mode {
	default:
		fallthrough
	case pagingWriterNormal:
		return w.w.Write(p)
	case pagingWriterMaybe:
		w.buf = append(w.buf, p...)
		if w.largeOutput() {
			w.cmd = exec.Command(w.pager)
			w.cmd.Stdout = os.Stdout
			w.cmd.Stderr = os.Stderr

			var err1, err2 error
			w.cmdStdin, err1 = w.cmd.StdinPipe()
			err2 = w.cmd.Start()
			if err1 != nil || err2 != nil {
				w.cmd = nil
				w.mode = pagingWriterNormal
				return w.w.Write(p)
			}
			if !w.lastnl {
				w.w.Write([]byte("\n"))
			}
			w.w.Write([]byte("Sending output to pager...\n"))
			w.cmdStdin.Write(w.buf)
			w.buf = nil
			w.mode = pagingWriterPaging
			return w.cmdStdin.Write(p)
		} else {
			if len(p) > 0 {
				w.lastnl = p[len(p)-1] == '\n'
			}
			return w.w.Write(p)
		}
	case pagingWriterPaging:
		n, err := w.cmdStdin.Write(p)
		if err != nil && w.cancel != nil {
			w.cancel()
			w.cancel = nil
		}
		return n, err
	}
}

// Reset returns the pagingWriter to its normal mode.
func (w *pagingWriter) Reset() {
	if w.mode == pagingWriterNormal {
		return
	}
	w.mode = pagingWriterNormal
	w.buf = nil
	if w.cmd != nil {
		w.cmdStdin.Close()
		w.cmd.Wait()
		w.cmd = nil
		w.cmdStdin = nil
	}
}

// PageMaybe configures pagingWriter to cache the output, after a large
// amount of text has been written to w it will automatically switch to
// piping output to a pager.
// The cancel function is called the first time a write to the pager errors.
func (w *pagingWriter) PageMaybe(cancel func()) {
	if w.mode != pagingWriterNormal {
		return
	}
	dlvpager := os.Getenv("DELVE_PAGER")
	if dlvpager == "" {
		if stdout, _ := w.w.(*os.File); stdout != nil {
			if !isatty.IsTerminal(stdout.Fd()) {
				return
			}
		}
		if strings.ToLower(os.Getenv("TERM")) == "dumb" {
			return
		}
	}
	w.mode = pagingWriterMaybe
	w.pager = dlvpager
	if w.pager == "" {
		w.pager = os.Getenv("PAGER")
		if w.pager == "" {
			w.pager = "more"
		}
	}
	w.lastnl = true
	w.cancel = cancel
}

func (w *pagingWriter) largeOutput() bool {
	if len(w.buf) > pagingWriterMaxLines*pagingWriterColsPerLine {
		return true
	}
	nl := 0
	for i := range w.buf {
		if w.buf[i] == '\n' {
			nl++
			if nl > pagingWriterMaxLines {
				return true
			}
		}
	}
	return false
}
