// Copyright 2016-2018, Pulumi Corporation.
//
// 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 diag

import (
	"bytes"
	"fmt"
	"io"
	"sync"

	"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
)

// Sink facilitates pluggable diagnostics messages.
type Sink interface {
	// Logf issues a log message.
	Logf(sev Severity, diag *Diag, args ...interface{})
	// Debugf issues a debugging message.
	Debugf(diag *Diag, args ...interface{})
	// Infof issues an informational message (to stdout).
	Infof(diag *Diag, args ...interface{})
	// Infoerrf issues an informational message (to stderr).
	Infoerrf(diag *Diag, args ...interface{})
	// Errorf issues a new error diagnostic.
	Errorf(diag *Diag, args ...interface{})
	// Warningf issues a new warning diagnostic.
	Warningf(diag *Diag, args ...interface{})

	// Stringify stringifies a diagnostic into a prefix and message that is appropriate for printing.
	Stringify(sev Severity, diag *Diag, args ...interface{}) (string, string)
}

// Severity dictates the kind of diagnostic.
type Severity string

const (
	Debug   Severity = "debug"
	Info    Severity = "info"
	Infoerr Severity = "info#err"
	Warning Severity = "warning"
	Error   Severity = "error"
)

// FormatOptions controls the output style and content.
type FormatOptions struct {
	Pwd   string              // the working directory.
	Color colors.Colorization // how output should be colorized.
	Debug bool                // if true, debugging will be output to stdout.
}

// DefaultSink returns a default sink that simply logs output to stderr/stdout.
func DefaultSink(stdout io.Writer, stderr io.Writer, opts FormatOptions) Sink {
	contract.Requiref(stdout != nil, "stdout", "must not be nil")
	contract.Requiref(stderr != nil, "stderr", "must not be nil")

	stdoutMu := &sync.Mutex{}
	stderrMu := &sync.Mutex{}
	func() {
		defer func() {
			// The == check below can panic if stdout and stderr are not comparable.
			// If that happens, ignore the panic and use separate mutexes.
			_ = recover()
		}()

		if stdout == stderr {
			// If stdout and stderr point to the same stream,
			// use the same mutex for them.
			stderrMu = stdoutMu
		}
	}()

	// Wrap the stdout and stderr writers in a mutex
	// to ensure that we don't interleave output.
	stdout = &syncWriter{Writer: stdout, mu: stdoutMu}
	stderr = &syncWriter{Writer: stderr, mu: stderrMu}

	// Discard debug output by default unless requested.
	debug := io.Discard
	if opts.Debug {
		debug = stdout
	}

	return newDefaultSink(opts, map[Severity]io.Writer{
		Debug:   debug,
		Info:    stdout,
		Infoerr: stderr,
		Error:   stderr,
		Warning: stderr,
	})
}

func newDefaultSink(opts FormatOptions, writers map[Severity]io.Writer) *defaultSink {
	contract.Assertf(writers[Debug] != nil, "Writer for %v must be set", Debug)
	contract.Assertf(writers[Info] != nil, "Writer for %v must be set", Info)
	contract.Assertf(writers[Infoerr] != nil, "Writer for %v must be set", Infoerr)
	contract.Assertf(writers[Error] != nil, "Writer for %v must be set", Error)
	contract.Assertf(writers[Warning] != nil, "Writer for %v must be set", Warning)
	contract.Assertf(opts.Color != "", "FormatOptions.Color must be set")
	return &defaultSink{
		opts:    opts,
		writers: writers,
	}
}

const DefaultSinkIDPrefix = "PU"

// defaultSink is the default sink which logs output to stderr/stdout.
type defaultSink struct {
	opts    FormatOptions          // a set of options that control output style and content.
	writers map[Severity]io.Writer // the writers to use for each kind of diagnostic severity.
}

func (d *defaultSink) Logf(sev Severity, diag *Diag, args ...interface{}) {
	switch sev {
	case Debug:
		d.Debugf(diag, args...)
	case Info:
		d.Infof(diag, args...)
	case Infoerr:
		d.Infoerrf(diag, args...)
	case Warning:
		d.Warningf(diag, args...)
	case Error:
		d.Errorf(diag, args...)
	default:
		contract.Failf("Unrecognized severity: %v", sev)
	}
}

func (d *defaultSink) createMessage(sev Severity, diag *Diag, args ...interface{}) string {
	prefix, msg := d.Stringify(sev, diag, args...)
	return prefix + msg
}

func (d *defaultSink) Debugf(diag *Diag, args ...interface{}) {
	// For debug messages, write both to the glogger and a stream, if there is one.
	logging.V(3).Infof(diag.Message, args...)
	msg := d.createMessage(Debug, diag, args...)
	if logging.V(9) {
		logging.V(9).Infof("defaultSink::Debug(%v)", msg[:len(msg)-1])
	}
	d.print(Debug, msg)
}

func (d *defaultSink) Infof(diag *Diag, args ...interface{}) {
	msg := d.createMessage(Info, diag, args...)
	if logging.V(5) {
		logging.V(5).Infof("defaultSink::Info(%v)", msg[:len(msg)-1])
	}
	d.print(Info, msg)
}

func (d *defaultSink) Infoerrf(diag *Diag, args ...interface{}) {
	msg := d.createMessage(Info /* not Infoerr, just "info: "*/, diag, args...)
	if logging.V(5) {
		logging.V(5).Infof("defaultSink::Infoerr(%v)", msg[:len(msg)-1])
	}
	d.print(Infoerr, msg)
}

func (d *defaultSink) Errorf(diag *Diag, args ...interface{}) {
	msg := d.createMessage(Error, diag, args...)
	if logging.V(5) {
		logging.V(5).Infof("defaultSink::Error(%v)", msg[:len(msg)-1])
	}
	d.print(Error, msg)
}

func (d *defaultSink) Warningf(diag *Diag, args ...interface{}) {
	msg := d.createMessage(Warning, diag, args...)
	if logging.V(5) {
		logging.V(5).Infof("defaultSink::Warning(%v)", msg[:len(msg)-1])
	}
	d.print(Warning, msg)
}

func (d *defaultSink) print(sev Severity, msg string) {
	fmt.Fprint(d.writers[sev], msg)
}

func (d *defaultSink) Stringify(sev Severity, diag *Diag, args ...interface{}) (string, string) {
	var prefix bytes.Buffer
	if sev != Info && sev != Infoerr {
		// Unless it's an ordinary stdout message, prepend the message category's prefix (error/warning).
		switch sev {
		case Debug:
			prefix.WriteString(colors.SpecDebug)
		case Error:
			prefix.WriteString(colors.SpecError)
		case Warning:
			prefix.WriteString(colors.SpecWarning)
		case Info, Infoerr:
			// We'll never get here, but the linter doesn't recognize that.
		default:
			contract.Failf("Unrecognized diagnostic severity: %v", sev)
		}

		prefix.WriteString(string(sev))
		prefix.WriteString(": ")
		prefix.WriteString(colors.Reset)
	}

	// Finally, actually print the message itself.
	var buffer bytes.Buffer
	buffer.WriteString(colors.SpecNote)

	if diag.Raw {
		buffer.WriteString(diag.Message)
	} else {
		fmt.Fprintf(&buffer, diag.Message, args...)
	}

	buffer.WriteString(colors.Reset)
	buffer.WriteRune('\n')

	// Ensure that any sensitive data we know about is filtered out preemptively.
	filtered := logging.FilterString(buffer.String())

	// If colorization was requested, compile and execute the directives now.
	return d.opts.Color.Colorize(prefix.String()), d.opts.Color.Colorize(filtered)
}

// syncWriter wraps an io.Writer and ensures that all writes are synchronized
// with a mutex.
type syncWriter struct {
	io.Writer

	mu *sync.Mutex
}

func (w *syncWriter) Write(p []byte) (int, error) {
	w.mu.Lock()
	defer w.mu.Unlock()
	return w.Writer.Write(p)
}