// Copyright © 2016 Asteris, LLC
//
// 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 human

import (
	"bytes"
	"fmt"
	"strings"
	"sync"
	"text/tabwriter"
	"text/template"

	"github.com/asteris-llc/converge/graph"
	pp "github.com/asteris-llc/converge/prettyprinters"
	"github.com/pkg/errors"
)

// Printer for human-readable output
type Printer struct {
	Color  bool // color output
	Filter FilterFunc
}

var (
	funcs   = map[string]interface{}{}
	funcsMu sync.Mutex
)

// New returns a base version of Printer
func New() *Printer {
	return NewFiltered(ShowEverything)
}

// NewFiltered returns a version of Printer that will filter according to the
// specified func
func NewFiltered(f FilterFunc) *Printer {
	return &Printer{Filter: f}
}

// InitColors initializes the colors used by the human printer
func (p *Printer) InitColors() {
	reset := "\x1b[0m"
	p.funcsMapWrite("bold", p.styled(func(in string) string { return "\x1b[1m" + in + reset }))
	p.funcsMapWrite("black", p.styled(func(in string) string { return "\x1b[30m" + in + reset }))
	p.funcsMapWrite("red", p.styled(func(in string) string { return "\x1b[31m" + in + reset }))
	p.funcsMapWrite("green", p.styled(func(in string) string { return "\x1b[32m" + in + reset }))
	p.funcsMapWrite("yellow", p.styled(func(in string) string { return "\x1b[33m" + in + reset }))
	p.funcsMapWrite("blue", p.styled(func(in string) string { return "\x1b[34m" + in + reset }))
	p.funcsMapWrite("magenta", p.styled(func(in string) string { return "\x1b[35m" + in + reset }))
	p.funcsMapWrite("cyan", p.styled(func(in string) string { return "\x1b[36m" + in + reset }))
	p.funcsMapWrite("white", p.styled(func(in string) string { return "\x1b[37m" + in + reset }))
}

// StartPP does nothing, but is required to satisfy the GraphPrinter interface
func (p *Printer) StartPP(g *graph.Graph) (pp.Renderable, error) {
	return pp.HiddenString(), nil
}

// FinishPP provides summary statistics about the printed graph
func (p *Printer) FinishPP(g *graph.Graph) (pp.Renderable, error) {
	tmpl, err := p.template(`{{if .Errors}}Errors:
{{range .Errors}} * {{.}}
{{end}}
{{end}}
{{- if .DependencyErrors}}Failed due to failing dependency:
{{range .DependencyErrors}} * {{.}}
{{end}}
{{end}}
{{- if gt (len .Errors) 0}}{{red "Summary"}}
{{- else}}{{green "Summary"}}
{{- end}}: {{len .Errors}} errors, {{.ChangesCount}} changes
{{- if .DependencyErrors}}, {{len .DependencyErrors}} dependency errors
{{- end}}
`)
	if err != nil {
		return pp.HiddenString(), err
	}

	counts := struct {
		ChangesCount     int
		Errors           []error
		DependencyErrors []error
	}{}

	for _, id := range g.Vertices() {
		meta, ok := g.Get(id)
		if !ok {
			continue
		}

		printable, ok := meta.Value().(Printable)
		if !ok {
			continue
		}

		if err = printable.Error(); err != nil {
			if id != "root" {
				if strings.Contains(err.Error(), "error in dependency") {
					counts.DependencyErrors = append(
						counts.DependencyErrors,
						errors.Wrap(err, id),
					)
				} else {
					counts.Errors = append(
						counts.Errors,
						errors.Wrap(err, id),
					)
				}
			}
		} else if printable.HasChanges() && id != "root" {
			counts.ChangesCount++
		}
	}

	var buf bytes.Buffer
	err = tmpl.Execute(&buf, counts)

	return &buf, err
}

// DrawNode containing a result
func (p *Printer) DrawNode(g *graph.Graph, id string) (pp.Renderable, error) {
	meta, ok := g.Get(id)
	if !ok {
		return pp.HiddenString(), nil
	}

	printable, ok := meta.Value().(Printable)
	if !ok {
		return pp.HiddenString(), errors.New("cannot print values that don't implement Printable")
	}

	if !p.Filter(id, printable) {
		return pp.HiddenString(), nil
	}

	tmpl, err := p.template(`{{if .Error}}{{red .ID}}{{else if .HasChanges}}{{yellow .ID}}{{else}}{{.ID}}{{end}}:
	{{- if .Error}}
	{{red "Error"}}: {{.Error}}
	{{- end}}
	{{- if .Warning}}
	{{yellow "Warning"}}: {{.Warning}}
	{{- end}}
	Messages:
	{{- range $msg := .Messages}}
	{{indent $msg}}
	{{- end}}
	Has Changes: {{if .HasChanges}}{{yellow "yes"}}{{else}}no{{end}}
	Changes:
		{{- range $key, $values := .Changes}}
		{{cyan $key}}:	{{diff ($values.Original) ($values.Current)}}
		{{- else}} No changes {{- end}}

`)
	if err != nil {
		return pp.HiddenString(), err
	}

	var intermediate, out bytes.Buffer
	err = tmpl.Execute(&intermediate, &printerNode{ID: id, Printable: printable})
	if err != nil {
		return pp.HiddenString(), err
	}

	tabWriter := tabwriter.NewWriter(&out, 1, 1, 1, ' ', 0)
	_, err = tabWriter.Write(intermediate.Bytes())

	return &out, err
}

func (p *Printer) getFunc(key string) func(string) string {
	funcsMu.Lock()
	defer funcsMu.Unlock()
	return funcs[key].(func(string) string)
}

func (p *Printer) funcsMapWrite(key string, value interface{}) {
	funcsMu.Lock()
	defer funcsMu.Unlock()
	funcs[key] = value
}

func (p *Printer) template(source string) (*template.Template, error) {
	p.funcsMapWrite("diff", p.diff)
	p.funcsMapWrite("indent", p.indent)
	p.funcsMapWrite("empty", p.empty)

	funcsMu.Lock()
	defer funcsMu.Unlock()
	return template.New("").Funcs(funcs).Parse(source)
}

func (p *Printer) styled(style func(string) string) func(string) string {
	if !p.Color {
		return func(in string) string { return in }
	}

	return style
}

func (p *Printer) diff(before, after string) (string, error) {
	// remember when modifying these that diff is responsible for leading
	// whitespace
	if !strings.Contains(strings.TrimSpace(before), "\n") && !strings.Contains(strings.TrimSpace(after), "\n") {
		return p.getFunc("bold")(
			fmt.Sprintf("%q\t=>\t%q", strings.TrimSpace(before), strings.TrimSpace(after)),
		), nil
	}

	tmpl, err := p.template(`before:
{{.Before}}
after:
{{.After}}`)
	if err != nil {
		return "", err
	}

	buf := new(bytes.Buffer)
	err = tmpl.Execute(buf, struct{ Before, After string }{before, after})

	return "\n" + p.indent(p.indent(buf.String())), err
}

func (p *Printer) indent(in string) string {
	return "\t" + strings.Replace(in, "\n", "\n\t", -1)
}
func (p *Printer) empty(s string) bool {
	return s == ""
}
