package zcl

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"

	wordwrap "github.com/mitchellh/go-wordwrap"
)

type diagnosticTextWriter struct {
	files map[string]*File
	wr    io.Writer
	width uint
	color bool
}

// NewDiagnosticTextWriter creates a DiagnosticWriter that writes diagnostics
// to the given writer as formatted text.
//
// It is designed to produce text appropriate to print in a monospaced font
// in a terminal of a particular width, or optionally with no width limit.
//
// The given width may be zero to disable word-wrapping of the detail text
// and truncation of source code snippets.
//
// If color is set to true, the output will include VT100 escape sequences to
// color-code the severity indicators. It is suggested to turn this off if
// the target writer is not a terminal.
func NewDiagnosticTextWriter(wr io.Writer, files map[string]*File, width uint, color bool) DiagnosticWriter {
	return &diagnosticTextWriter{
		files: files,
		wr:    wr,
		width: width,
		color: color,
	}
}

func (w *diagnosticTextWriter) WriteDiagnostic(diag *Diagnostic) error {
	if diag == nil {
		return errors.New("nil diagnostic")
	}

	var colorCode, resetCode string
	if w.color {
		switch diag.Severity {
		case DiagError:
			colorCode = "\x1b[31m"
		case DiagWarning:
			colorCode = "\x1b[33m"
		}
		resetCode = "\x1b[0m"
	}

	var severityStr string
	switch diag.Severity {
	case DiagError:
		severityStr = "Error"
	case DiagWarning:
		severityStr = "Warning"
	default:
		// should never happen
		severityStr = "???????"
	}

	fmt.Fprintf(w.wr, "%s%s%s: %s\n\n", colorCode, severityStr, resetCode, diag.Summary)

	if diag.Subject != nil {

		file := w.files[diag.Subject.Filename]
		if file == nil || file.Bytes == nil {
			fmt.Fprintf(w.wr, "  on %s line %d:\n  (source code not available)\n\n", diag.Subject.Filename, diag.Subject.Start.Line)
		} else {
			src := file.Bytes
			r := bytes.NewReader(src)
			sc := bufio.NewScanner(r)
			sc.Split(bufio.ScanLines)

			var startLine, endLine int
			if diag.Context != nil {
				startLine = diag.Context.Start.Line
				endLine = diag.Context.End.Line
			} else {
				startLine = diag.Subject.Start.Line
				endLine = diag.Subject.End.Line
			}

			var contextLine string
			if diag.Subject != nil {
				contextLine = contextString(file, diag.Subject.Start.Byte)
				if contextLine != "" {
					contextLine = ", in " + contextLine
				}
			}

			li := 1
			var ls string
			for sc.Scan() {
				ls = sc.Text()

				if li == startLine {
					break
				}
				li++
			}

			fmt.Fprintf(w.wr, "  on %s line %d%s:\n", diag.Subject.Filename, diag.Subject.Start.Line, contextLine)

			// TODO: Generate markers for the specific characters that are in the Context and Subject ranges.
			// For now, we just print out the lines.

			fmt.Fprintf(w.wr, "%4d: %s\n", li, ls)

			if endLine > li {
				for sc.Scan() {
					ls = sc.Text()
					li++

					fmt.Fprintf(w.wr, "%4d: %s\n", li, ls)

					if li == endLine {
						break
					}
				}
			}

			w.wr.Write([]byte{'\n'})
		}
	}

	if diag.Detail != "" {
		detail := diag.Detail
		if w.width != 0 {
			detail = wordwrap.WrapString(detail, w.width)
		}
		fmt.Fprintf(w.wr, "%s\n\n", detail)
	}

	return nil
}

func (w *diagnosticTextWriter) WriteDiagnostics(diags Diagnostics) error {
	for _, diag := range diags {
		err := w.WriteDiagnostic(diag)
		if err != nil {
			return err
		}
	}
	return nil
}

func contextString(file *File, offset int) string {
	type contextStringer interface {
		ContextString(offset int) string
	}

	if cser, ok := file.Nav.(contextStringer); ok {
		return cser.ContextString(offset)
	}
	return ""
}