package main

import (
	"reflect"
	"strings"

	"github.com/containers/buildah/pkg/formats"
	"github.com/containers/libpod/cmd/podman/cliconfig"
	"github.com/containers/libpod/pkg/adapter"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
)

// volumeOptions is the "ls" command options
type volumeLsOptions struct {
	Format string
	Quiet  bool
}

// volumeLsTemplateParams is the template parameters to list the volumes
type volumeLsTemplateParams struct {
	Name       string
	Labels     string
	MountPoint string
	Driver     string
	Options    string
	Scope      string
}

// volumeLsJSONParams is the JSON parameters to list the volumes
type volumeLsJSONParams struct {
	Name       string            `json:"name"`
	Labels     map[string]string `json:"labels"`
	MountPoint string            `json:"mountPoint"`
	Driver     string            `json:"driver"`
	Options    map[string]string `json:"options"`
	Scope      string            `json:"scope"`
}

var (
	volumeLsCommand cliconfig.VolumeLsValues

	volumeLsDescription = `
podman volume ls

List all available volumes. The output of the volumes can be filtered
and the output format can be changed to JSON or a user specified Go template.`
	_volumeLsCommand = &cobra.Command{
		Use:     "ls",
		Aliases: []string{"list"},
		Args:    noSubArgs,
		Short:   "List volumes",
		Long:    volumeLsDescription,
		RunE: func(cmd *cobra.Command, args []string) error {
			volumeLsCommand.InputArgs = args
			volumeLsCommand.GlobalFlags = MainGlobalOpts
			return volumeLsCmd(&volumeLsCommand)
		},
	}
)

func init() {
	volumeLsCommand.Command = _volumeLsCommand
	volumeLsCommand.SetHelpTemplate(HelpTemplate())
	volumeLsCommand.SetUsageTemplate(UsageTemplate())
	flags := volumeLsCommand.Flags()

	flags.StringVarP(&volumeLsCommand.Filter, "filter", "f", "", "Filter volume output")
	flags.StringVar(&volumeLsCommand.Format, "format", "table {{.Driver}}\t{{.Name}}", "Format volume output using Go template")
	flags.BoolVarP(&volumeLsCommand.Quiet, "quiet", "q", false, "Print volume output in quiet mode")
}

func volumeLsCmd(c *cliconfig.VolumeLsValues) error {
	runtime, err := adapter.GetRuntime(&c.PodmanCommand)
	if err != nil {
		return errors.Wrapf(err, "error creating libpod runtime")
	}
	defer runtime.Shutdown(false)

	opts := volumeLsOptions{
		Quiet: c.Quiet,
	}
	opts.Format = genVolLsFormat(c)

	// Get the filter functions based on any filters set
	var filterFuncs []adapter.VolumeFilter
	if c.Filter != "" {
		filters := strings.Split(c.Filter, ",")
		for _, f := range filters {
			filterSplit := strings.Split(f, "=")
			if len(filterSplit) < 2 {
				return errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f)
			}
			generatedFunc, err := generateVolumeFilterFuncs(filterSplit[0], filterSplit[1])
			if err != nil {
				return errors.Wrapf(err, "invalid filter")
			}
			filterFuncs = append(filterFuncs, generatedFunc)
		}
	}

	volumes, err := runtime.Volumes(getContext())
	if err != nil {
		return err
	}
	// Get the volumes that match the filter
	volsFiltered := make([]*adapter.Volume, 0, len(volumes))
	for _, vol := range volumes {
		include := true
		for _, filter := range filterFuncs {
			include = include && filter(vol)
		}

		if include {
			volsFiltered = append(volsFiltered, vol)
		}
	}
	return generateVolLsOutput(volsFiltered, opts)
}

// generate the template based on conditions given
func genVolLsFormat(c *cliconfig.VolumeLsValues) string {
	var format string
	if c.Format != "" {
		// "\t" from the command line is not being recognized as a tab
		// replacing the string "\t" to a tab character if the user passes in "\t"
		format = strings.Replace(c.Format, `\t`, "\t", -1)
	}
	if c.Quiet {
		format = "{{.Name}}"
	}
	return format
}

// Convert output to genericParams for printing
func volLsToGeneric(templParams []volumeLsTemplateParams, JSONParams []volumeLsJSONParams) (genericParams []interface{}) {
	if len(templParams) > 0 {
		for _, v := range templParams {
			genericParams = append(genericParams, interface{}(v))
		}
		return
	}
	for _, v := range JSONParams {
		genericParams = append(genericParams, interface{}(v))
	}
	return
}

// generate the accurate header based on template given
func (vol *volumeLsTemplateParams) volHeaderMap() map[string]string {
	v := reflect.Indirect(reflect.ValueOf(vol))
	values := make(map[string]string)

	for i := 0; i < v.NumField(); i++ {
		key := v.Type().Field(i).Name
		value := key
		if value == "Name" {
			value = "Volume" + value
		}
		values[key] = strings.ToUpper(splitCamelCase(value))
	}
	return values
}

// getVolTemplateOutput returns all the volumes in the volumeLsTemplateParams format
func getVolTemplateOutput(lsParams []volumeLsJSONParams, opts volumeLsOptions) ([]volumeLsTemplateParams, error) {
	var lsOutput []volumeLsTemplateParams

	for _, lsParam := range lsParams {
		var (
			labels  string
			options string
		)

		for k, v := range lsParam.Labels {
			label := k
			if v != "" {
				label += "=" + v
			}
			labels += label
		}
		for k, v := range lsParam.Options {
			option := k
			if v != "" {
				option += "=" + v
			}
			options += option
		}
		params := volumeLsTemplateParams{
			Name:       lsParam.Name,
			Driver:     lsParam.Driver,
			MountPoint: lsParam.MountPoint,
			Scope:      lsParam.Scope,
			Labels:     labels,
			Options:    options,
		}

		lsOutput = append(lsOutput, params)
	}
	return lsOutput, nil
}

// getVolJSONParams returns the volumes in JSON format
func getVolJSONParams(volumes []*adapter.Volume) []volumeLsJSONParams {
	var lsOutput []volumeLsJSONParams

	for _, volume := range volumes {
		params := volumeLsJSONParams{
			Name:       volume.Name(),
			Labels:     volume.Labels(),
			MountPoint: volume.MountPoint(),
			Driver:     volume.Driver(),
			Options:    volume.Options(),
			Scope:      volume.Scope(),
		}

		lsOutput = append(lsOutput, params)
	}
	return lsOutput
}

// generateVolLsOutput generates the output based on the format, JSON or Go Template, and prints it out
func generateVolLsOutput(volumes []*adapter.Volume, opts volumeLsOptions) error {
	if len(volumes) == 0 && opts.Format != formats.JSONString {
		return nil
	}
	lsOutput := getVolJSONParams(volumes)
	var out formats.Writer

	switch opts.Format {
	case formats.JSONString:
		out = formats.JSONStructArray{Output: volLsToGeneric([]volumeLsTemplateParams{}, lsOutput)}
	default:
		lsOutput, err := getVolTemplateOutput(lsOutput, opts)
		if err != nil {
			return errors.Wrapf(err, "unable to create volume output")
		}
		out = formats.StdoutTemplateArray{Output: volLsToGeneric(lsOutput, []volumeLsJSONParams{}), Template: opts.Format, Fields: lsOutput[0].volHeaderMap()}
	}
	return formats.Writer(out).Out()
}

// generateVolumeFilterFuncs returns the true if the volume matches the filter set, otherwise it returns false.
func generateVolumeFilterFuncs(filter, filterValue string) (func(volume *adapter.Volume) bool, error) {
	switch filter {
	case "name":
		return func(v *adapter.Volume) bool {
			return strings.Contains(v.Name(), filterValue)
		}, nil
	case "driver":
		return func(v *adapter.Volume) bool {
			return v.Driver() == filterValue
		}, nil
	case "scope":
		return func(v *adapter.Volume) bool {
			return v.Scope() == filterValue
		}, nil
	case "label":
		filterArray := strings.SplitN(filterValue, "=", 2)
		filterKey := filterArray[0]
		if len(filterArray) > 1 {
			filterValue = filterArray[1]
		} else {
			filterValue = ""
		}
		return func(v *adapter.Volume) bool {
			for labelKey, labelValue := range v.Labels() {
				if labelKey == filterKey && ("" == filterValue || labelValue == filterValue) {
					return true
				}
			}
			return false
		}, nil
	case "opt":
		filterArray := strings.SplitN(filterValue, "=", 2)
		filterKey := filterArray[0]
		if len(filterArray) > 1 {
			filterValue = filterArray[1]
		} else {
			filterValue = ""
		}
		return func(v *adapter.Volume) bool {
			for labelKey, labelValue := range v.Options() {
				if labelKey == filterKey && ("" == filterValue || labelValue == filterValue) {
					return true
				}
			}
			return false
		}, nil
	}
	return nil, errors.Errorf("%s is an invalid filter", filter)
}