package inspect

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"text/template"
)

// Inspector defines an interface to implement to process elements
type Inspector interface {
	Inspect(typedElement interface{}, rawElement []byte) error
	Flush() error
}

// TemplateInspector uses a text template to inspect elements.
type TemplateInspector struct {
	outputStream io.Writer
	buffer       *bytes.Buffer
	tmpl         *template.Template
}

// NewTemplateInspector creates a new inspector with a template.
func NewTemplateInspector(outputStream io.Writer, tmpl *template.Template) Inspector {
	return &TemplateInspector{
		outputStream: outputStream,
		buffer:       new(bytes.Buffer),
		tmpl:         tmpl,
	}
}

// Inspect executes the inspect template.
// It decodes the raw element into a map if the initial execution fails.
// This allows docker cli to parse inspect structs injected with Swarm fields.
func (i *TemplateInspector) Inspect(typedElement interface{}, rawElement []byte) error {
	buffer := new(bytes.Buffer)
	if err := i.tmpl.Execute(buffer, typedElement); err != nil {
		if rawElement == nil {
			return fmt.Errorf("Template parsing error: %v", err)
		}
		return i.tryRawInspectFallback(rawElement, err)
	}
	i.buffer.Write(buffer.Bytes())
	i.buffer.WriteByte('\n')
	return nil
}

// Flush write the result of inspecting all elements into the output stream.
func (i *TemplateInspector) Flush() error {
	if i.buffer.Len() == 0 {
		_, err := io.WriteString(i.outputStream, "\n")
		return err
	}
	_, err := io.Copy(i.outputStream, i.buffer)
	return err
}

// IndentedInspector uses a buffer to stop the indented representation of an element.
type IndentedInspector struct {
	outputStream io.Writer
	elements     []interface{}
	rawElements  [][]byte
}

// NewIndentedInspector generates a new IndentedInspector.
func NewIndentedInspector(outputStream io.Writer) Inspector {
	return &IndentedInspector{
		outputStream: outputStream,
	}
}

// Inspect writes the raw element with an indented json format.
func (i *IndentedInspector) Inspect(typedElement interface{}, rawElement []byte) error {
	if rawElement != nil {
		i.rawElements = append(i.rawElements, rawElement)
	} else {
		i.elements = append(i.elements, typedElement)
	}
	return nil
}

// Flush write the result of inspecting all elements into the output stream.
func (i *IndentedInspector) Flush() error {
	if len(i.elements) == 0 && len(i.rawElements) == 0 {
		_, err := io.WriteString(i.outputStream, "[]\n")
		return err
	}

	var buffer io.Reader
	if len(i.rawElements) > 0 {
		bytesBuffer := new(bytes.Buffer)
		bytesBuffer.WriteString("[")
		for idx, r := range i.rawElements {
			bytesBuffer.Write(r)
			if idx < len(i.rawElements)-1 {
				bytesBuffer.WriteString(",")
			}
		}
		bytesBuffer.WriteString("]")
		indented := new(bytes.Buffer)
		if err := json.Indent(indented, bytesBuffer.Bytes(), "", "    "); err != nil {
			return err
		}
		buffer = indented
	} else {
		b, err := json.MarshalIndent(i.elements, "", "    ")
		if err != nil {
			return err
		}
		buffer = bytes.NewReader(b)
	}

	if _, err := io.Copy(i.outputStream, buffer); err != nil {
		return err
	}
	_, err := io.WriteString(i.outputStream, "\n")
	return err
}