// Copyright 2012, 2013 Canonical Ltd.
// Licensed under the LGPLv3, see LICENSE file for details.

package cmd

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"sort"
	"strings"

	"github.com/juju/gnuflag"
	goyaml "gopkg.in/yaml.v2"
)

// Formatter writes the arbitrary object into the writer.
type Formatter func(writer io.Writer, value interface{}) error

// FormatYaml writes out value as yaml to the writer, unless value is nil.
func FormatYaml(writer io.Writer, value interface{}) error {
	if value == nil {
		return nil
	}
	result, err := goyaml.Marshal(value)
	if err != nil {
		return err
	}
	for i := len(result) - 1; i > 0; i-- {
		if result[i] != '\n' {
			break
		}
		result = result[:i]
	}

	if len(result) > 0 {
		result = append(result, '\n')
		_, err = writer.Write(result)
		return err
	}
	return nil
}

// FormatJson writes out value as json.
func FormatJson(writer io.Writer, value interface{}) error {
	result, err := json.Marshal(value)
	if err != nil {
		return err
	}
	result = append(result, '\n')
	_, err = writer.Write(result)
	return err
}

// FormatSmart marshals value into a []byte according to the following rules:
//   * string:        untouched
//   * bool:          converted to `True` or `False` (to match pyjuju)
//   * int or float:  converted to sensible strings
//   * []string:      joined by `\n`s into a single string
//   * anything else: delegate to FormatYaml
func FormatSmart(writer io.Writer, value interface{}) error {
	if value == nil {
		return nil
	}
	valueStr := ""
	switch value := value.(type) {
	case string:
		valueStr = value
	case []string:
		valueStr = strings.Join(value, "\n")
	case bool:
		if value {
			valueStr = "True"
		} else {
			valueStr = "False"
		}
	default:
		return FormatYaml(writer, value)
	}
	if valueStr == "" {
		return nil
	}
	_, err := writer.Write([]byte(valueStr + "\n"))
	return err
}

// TypeFormatter describes a formatting type that can define if a type is
// serialisable.
type TypeFormatter struct {
	Formatter    Formatter
	Serialisable bool
}

type formatters map[string]TypeFormatter

// Formatters returns the underlying formatters without the additional
// information of a TypeFormatter.
func (f formatters) Formatters() map[string]Formatter {
	result := make(map[string]Formatter, len(f))
	for k, v := range f {
		result[k] = v.Formatter
	}
	return result
}

// DefaultFormatters holds the formatters that can be
// specified with the --format flag.
var DefaultFormatters = formatters{
	"smart": TypeFormatter{Formatter: FormatSmart, Serialisable: false},
	"yaml":  TypeFormatter{Formatter: FormatYaml, Serialisable: true},
	"json":  TypeFormatter{Formatter: FormatJson, Serialisable: true},
}

// formatterValue implements gnuflag.Value for the --format flag.
type formatterValue struct {
	name       string
	formatters map[string]Formatter
}

// newFormatterValue returns a new formatterValue. The initial Formatter name
// must be present in formatters.
func newFormatterValue(initial string, formatters map[string]Formatter) *formatterValue {
	v := &formatterValue{formatters: formatters}
	if err := v.Set(initial); err != nil {
		panic(err)
	}
	return v
}

// Set stores the chosen formatter name in v.name.
func (v *formatterValue) Set(value string) error {
	if v.formatters[value] == nil {
		return fmt.Errorf("unknown format %q", value)
	}
	v.name = value
	return nil
}

// String returns the chosen formatter name.
func (v *formatterValue) String() string {
	return v.name
}

// doc returns documentation for the --format flag.
func (v *formatterValue) doc() string {
	choices := make([]string, len(v.formatters))
	i := 0
	for name := range v.formatters {
		choices[i] = name
		i++
	}
	sort.Strings(choices)
	return "Specify output format (" + strings.Join(choices, "|") + ")"
}

// format runs the chosen formatter on value.
func (v *formatterValue) format(writer io.Writer, value interface{}) error {
	return v.formatters[v.name](writer, value)
}

// Output is responsible for interpreting output-related command line flags
// and writing a value to a file or to stdout as directed.
type Output struct {
	formatter *formatterValue
	outPath   string
}

// AddFlags injects the --format and --output command line flags into f.
func (c *Output) AddFlags(f *gnuflag.FlagSet, defaultFormatter string, formatters map[string]Formatter) {
	c.formatter = newFormatterValue(defaultFormatter, formatters)
	f.Var(c.formatter, "format", c.formatter.doc())
	f.StringVar(&c.outPath, "o", "", "Specify an output file")
	f.StringVar(&c.outPath, "output", "", "")
}

// Write formats and outputs the value as directed by the --format and
// --output command line flags.
func (c *Output) Write(ctx *Context, value interface{}) (err error) {
	formatterName := c.formatter.name
	formatter := c.formatter.formatters[formatterName]
	if err := c.writeFormatter(ctx, formatter, value); err != nil {
		return err
	}
	return nil
}

// WriteFormatter formats and outputs the value with the given formatter,
// to the output directed by the --output command line flag.
func (c *Output) WriteFormatter(ctx *Context, formatter Formatter, value interface{}) (err error) {
	return c.writeFormatter(ctx, formatter, value)
}

func (c *Output) writeFormatter(ctx *Context, formatter Formatter, value interface{}) (err error) {
	var target io.Writer
	if c.outPath == "" {
		target = ctx.Stdout
	} else {
		path := ctx.AbsPath(c.outPath)
		var f *os.File
		if f, err = os.Create(path); err != nil {
			return
		}
		defer f.Close()
		target = f
	}
	if err := formatter(target, value); err != nil {
		return err
	}
	// Suppress the handling of errors on stdout when a machine formatter is used.
	ctx.outputFormatUsed = true
	return nil
}

// Name returns the underlying name of the formatter.
func (c *Output) Name() string {
	return c.formatter.name
}
