// Copyright 2016 CoreOS Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package progressutil

import (
	"fmt"
	"io"
	"os"
	"strings"
	"sync"

	"golang.org/x/crypto/ssh/terminal"
)

var (
	// ErrorProgressOutOfBounds is returned if the progress is set to a value
	// not between 0 and 1.
	ErrorProgressOutOfBounds = fmt.Errorf("progress is out of bounds (0 to 1)")

	// ErrorNoBarsAdded is returned when no progress bars have been added to a
	// ProgressBarPrinter before PrintAndWait is called.
	ErrorNoBarsAdded = fmt.Errorf("AddProgressBar hasn't been called yet")
)

// ProgressBar represents one progress bar in a ProgressBarPrinter. Should not
// be created directly, use the AddProgressBar on a ProgressBarPrinter to
// create these.
type ProgressBar struct {
	lock sync.Mutex

	currentProgress float64
	printBefore     string
	printAfter      string
	done            bool
}

func (pb *ProgressBar) clone() *ProgressBar {
	pb.lock.Lock()
	pbClone := &ProgressBar{
		currentProgress: pb.currentProgress,
		printBefore:     pb.printBefore,
		printAfter:      pb.printAfter,
		done:            pb.done,
	}
	pb.lock.Unlock()
	return pbClone
}

func (pb *ProgressBar) GetCurrentProgress() float64 {
	pb.lock.Lock()
	val := pb.currentProgress
	pb.lock.Unlock()
	return val
}

// SetCurrentProgress sets the progress of this ProgressBar. The progress must
// be between 0 and 1 inclusive.
func (pb *ProgressBar) SetCurrentProgress(progress float64) error {
	if progress < 0 || progress > 1 {
		return ErrorProgressOutOfBounds
	}
	pb.lock.Lock()
	pb.currentProgress = progress
	pb.lock.Unlock()
	return nil
}

// GetDone returns whether or not this progress bar is done
func (pb *ProgressBar) GetDone() bool {
	pb.lock.Lock()
	val := pb.done
	pb.lock.Unlock()
	return val
}

// SetDone sets whether or not this progress bar is done
func (pb *ProgressBar) SetDone(val bool) {
	pb.lock.Lock()
	pb.done = val
	pb.lock.Unlock()
}

// GetPrintBefore gets the text printed on the line before the progress bar.
func (pb *ProgressBar) GetPrintBefore() string {
	pb.lock.Lock()
	val := pb.printBefore
	pb.lock.Unlock()
	return val
}

// SetPrintBefore sets the text printed on the line before the progress bar.
func (pb *ProgressBar) SetPrintBefore(before string) {
	pb.lock.Lock()
	pb.printBefore = before
	pb.lock.Unlock()
}

// GetPrintAfter gets the text printed on the line after the progress bar.
func (pb *ProgressBar) GetPrintAfter() string {
	pb.lock.Lock()
	val := pb.printAfter
	pb.lock.Unlock()
	return val
}

// SetPrintAfter sets the text printed on the line after the progress bar.
func (pb *ProgressBar) SetPrintAfter(after string) {
	pb.lock.Lock()
	pb.printAfter = after
	pb.lock.Unlock()
}

// ProgressBarPrinter will print out the progress of some number of
// ProgressBars.
type ProgressBarPrinter struct {
	lock sync.Mutex

	// DisplayWidth can be set to influence how large the progress bars are.
	// The bars will be scaled to attempt to produce lines of this number of
	// characters, but lines of different lengths may still be printed. When
	// this value is 0 (aka unset), 80 character columns are assumed.
	DisplayWidth int
	// PadToBeEven, when set to true, will make Print pad the printBefore text
	// with trailing spaces and the printAfter text with leading spaces to make
	// the progress bars the same length.
	PadToBeEven         bool
	numLinesInLastPrint int
	progressBars        []*ProgressBar
	maxBefore           int
	maxAfter            int
}

// AddProgressBar will create a new ProgressBar, register it with this
// ProgressBarPrinter, and return it. This must be called at least once before
// PrintAndWait is called.
func (pbp *ProgressBarPrinter) AddProgressBar() *ProgressBar {
	pb := &ProgressBar{}
	pbp.lock.Lock()
	pbp.progressBars = append(pbp.progressBars, pb)
	pbp.lock.Unlock()
	return pb
}

// Print will print out progress information for each ProgressBar that has been
// added to this ProgressBarPrinter. The progress will be written to printTo,
// and if printTo is a terminal it will draw progress bars.  AddProgressBar
// must be called at least once before Print is called. If printing to a
// terminal, all draws after the first one will move the cursor up to draw over
// the previously printed bars.
func (pbp *ProgressBarPrinter) Print(printTo io.Writer) (bool, error) {
	pbp.lock.Lock()
	var bars []*ProgressBar
	for _, bar := range pbp.progressBars {
		bars = append(bars, bar.clone())
	}
	numColumns := pbp.DisplayWidth
	pbp.lock.Unlock()

	if len(bars) == 0 {
		return false, ErrorNoBarsAdded
	}

	if numColumns == 0 {
		numColumns = 80
	}

	if isTerminal(printTo) {
		moveCursorUp(printTo, pbp.numLinesInLastPrint)
	}

	for _, bar := range bars {
		beforeSize := len(bar.GetPrintBefore())
		afterSize := len(bar.GetPrintAfter())
		if beforeSize > pbp.maxBefore {
			pbp.maxBefore = beforeSize
		}
		if afterSize > pbp.maxAfter {
			pbp.maxAfter = afterSize
		}
	}

	allDone := true
	for _, bar := range bars {
		if isTerminal(printTo) {
			bar.printToTerminal(printTo, numColumns, pbp.PadToBeEven, pbp.maxBefore, pbp.maxAfter)
		} else {
			bar.printToNonTerminal(printTo)
		}
		allDone = allDone && bar.GetCurrentProgress() == 1
	}

	pbp.numLinesInLastPrint = len(bars)

	return allDone, nil
}

// moveCursorUp moves the cursor up numLines in the terminal
func moveCursorUp(printTo io.Writer, numLines int) {
	if numLines > 0 {
		fmt.Fprintf(printTo, "\033[%dA", numLines)
	}
}

func (pb *ProgressBar) printToTerminal(printTo io.Writer, numColumns int, padding bool, maxBefore, maxAfter int) {
	before := pb.GetPrintBefore()
	after := pb.GetPrintAfter()

	if padding {
		before = before + strings.Repeat(" ", maxBefore-len(before))
		after = strings.Repeat(" ", maxAfter-len(after)) + after
	}

	progressBarSize := numColumns - (len(fmt.Sprintf("%s [] %s", before, after)))
	progressBar := ""
	if progressBarSize > 0 {
		currentProgress := int(pb.GetCurrentProgress() * float64(progressBarSize))
		progressBar = fmt.Sprintf("[%s%s] ",
			strings.Repeat("=", currentProgress),
			strings.Repeat(" ", progressBarSize-currentProgress))
	} else {
		// If we can't fit the progress bar, better to not pad the before/after.
		before = pb.GetPrintBefore()
		after = pb.GetPrintAfter()
	}

	fmt.Fprintf(printTo, "%s %s%s\n", before, progressBar, after)
}

func (pb *ProgressBar) printToNonTerminal(printTo io.Writer) {
	if !pb.GetDone() {
		fmt.Fprintf(printTo, "%s %s\n", pb.printBefore, pb.printAfter)
		if pb.GetCurrentProgress() == 1 {
			pb.SetDone(true)
		}
	}
}

// isTerminal returns True when w is going to a tty, and false otherwise.
func isTerminal(w io.Writer) bool {
	if f, ok := w.(*os.File); ok {
		return terminal.IsTerminal(int(f.Fd()))
	}
	return false
}
