// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// Package syncbuffer provides a goroutine safe bytes.Buffer as well printing functionality to the terminal.
package syncbuffer

import (
	"fmt"
	"io"
	"math"
	"strings"

	"github.com/aws/copilot-cli/internal/pkg/term/cursor"
	"golang.org/x/term"
)

// printAllLinesInBuf represents to print the entire contents in the buffer.
const (
	printAllLinesInBuf = -1
)

const (
	defaultTerminalWidth = 80
)

// FileWriter is the interface to write to a file.
type FileWriter interface {
	io.Writer
	Fd() uintptr
}

// LabeledTermPrinter is a printer to display label and logs to the terminal.
type LabeledTermPrinter struct {
	term             FileWriter           // term writes logs to the terminal FileWriter.
	buffers          []*LabeledSyncBuffer // buffers stores logs before writing to the terminal.
	numLines         int                  // number of lines that has to be written from each buffer.
	padding          int                  // Leading spaces before rendering to terminal.
	prevWrittenLines int                  // number of lines written from all the buffers.
}

// LabeledTermPrinterOption is a type alias to configure LabeledTermPrinter.
type LabeledTermPrinterOption func(ltp *LabeledTermPrinter)

// NewLabeledTermPrinter returns a LabeledTermPrinter that can print to the terminal filewriter from buffers.
func NewLabeledTermPrinter(fw FileWriter, bufs []*LabeledSyncBuffer, opts ...LabeledTermPrinterOption) *LabeledTermPrinter {
	ltp := &LabeledTermPrinter{
		term:     fw,
		buffers:  bufs,
		numLines: printAllLinesInBuf, // By default set numlines to -1 to print all from buffers.
	}
	for _, opt := range opts {
		opt(ltp)
	}
	return ltp
}

// WithNumLines sets the numlines of LabeledTermPrinter.
func WithNumLines(n int) LabeledTermPrinterOption {
	return func(ltp *LabeledTermPrinter) {
		ltp.numLines = n
	}
}

// WithPadding sets the padding of LabeledTermPrinter.
func WithPadding(n int) LabeledTermPrinterOption {
	return func(ltp *LabeledTermPrinter) {
		ltp.padding = n
	}
}

// IsDone returns true if all the buffers are done.
func (ltp *LabeledTermPrinter) IsDone() bool {
	for _, buf := range ltp.buffers {
		if !buf.IsDone() {
			return false
		}
	}
	return true
}

// Print prints the label and the last N lines of logs from each buffer
// to the LabeledTermPrinter fileWriter and erases the previous output.
// If numLines is -1 then print all the values from buffers.
func (ltp *LabeledTermPrinter) Print() {
	if ltp.numLines == printAllLinesInBuf {
		ltp.printAll()
		return
	}
	if ltp.prevWrittenLines > 0 {
		cursor.EraseLinesAbove(ltp.term, ltp.prevWrittenLines)
	}
	ltp.prevWrittenLines = 0
	for _, buf := range ltp.buffers {
		logs := buf.lines()
		outputLogs := ltp.lastNLines(logs)
		ltp.prevWrittenLines += ltp.writeLines(buf.label, outputLogs)
	}
}

// printAll writes the entire contents of all the buffers to the file writer.
// If one of the buffer gets done then print entire content of the buffer.
// Until all the buffers are written to file writer.
func (ltp *LabeledTermPrinter) printAll() {
	for idx := 0; idx < len(ltp.buffers); idx++ {
		if !ltp.buffers[idx].IsDone() {
			continue
		}
		outputLogs := ltp.buffers[idx].lines()
		ltp.writeLines(ltp.buffers[idx].label, outputLogs)
		ltp.buffers = append(ltp.buffers[:idx], ltp.buffers[idx+1:]...)
		idx--
	}
}

// lastNLines returns the last N lines of the given logs where N is the value of tp.numLines.
// If the logs slice contains fewer than N lines, all lines are returned.
// If the given input logs are empty then return slice of empty strings.
func (ltp *LabeledTermPrinter) lastNLines(logs []string) []string {
	var start int
	if len(logs) > ltp.numLines {
		start = len(logs) - ltp.numLines
	}
	end := len(logs)

	// Extract the last N lines
	logLines := make([]string, ltp.numLines)
	idx := 0
	for start < end {
		logLines[idx] = logs[start]
		start++
		idx++
	}
	return logLines
}

// writeLines writes a label and output logs to the terminal associated with the TermPrinter.
// Returns the number of lines needed to erase based on terminal width.
func (ltp *LabeledTermPrinter) writeLines(label string, lines []string) int {
	var numLines float64
	writeLine := func(line string) {
		fmt.Fprintln(ltp.term, line)
		if len(line) == 0 {
			numLines++
			return
		}
		numLines += math.Ceil(float64(len(line)) / float64(terminalWidth(ltp.term)))
	}
	writeLine(label)
	for _, line := range lines {
		writeLine(fmt.Sprintf("%s%s", strings.Repeat(" ", ltp.padding), line))
	}
	return int(numLines)
}

// terminalWidth returns the width of the terminal associated with the given FileWriter.
// If the FileWriter is not associated with a terminal, it returns a default terminal width.
func terminalWidth(fw FileWriter) int {
	terminalWidth := defaultTerminalWidth
	if term.IsTerminal(int(fw.Fd())) {
		// Swallow the error as we do not want propogate the error up to call stack.
		if width, _, err := term.GetSize(int(fw.Fd())); err == nil {
			terminalWidth = width
		}
	}
	return terminalWidth
}