package main

import (
	"context"
	"fmt"
	"reflect"
	"sort"
	"strings"
	"time"
	"unicode"

	"github.com/containers/buildah/pkg/formats"
	"github.com/containers/libpod/cmd/podman/cliconfig"
	"github.com/containers/libpod/libpod/image"
	"github.com/containers/libpod/pkg/adapter"
	"github.com/docker/go-units"
	"github.com/opencontainers/go-digest"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
)

type imagesTemplateParams struct {
	Repository  string
	Tag         string
	ID          string
	Digest      digest.Digest
	Digests     []digest.Digest
	Created     string
	CreatedTime time.Time
	Size        string
	ReadOnly    bool
	History     string
}

type imagesJSONParams struct {
	ID       string          `json:"id"`
	Name     []string        `json:"names"`
	Digest   digest.Digest   `json:"digest"`
	Digests  []digest.Digest `json:"digests"`
	Created  time.Time       `json:"created"`
	Size     *uint64         `json:"size"`
	ReadOnly bool            `json:"readonly"`
	History  []string        `json:"history"`
}

type imagesOptions struct {
	quiet        bool
	noHeading    bool
	noTrunc      bool
	digests      bool
	format       string
	outputformat string
	sort         string
	all          bool
	history      bool
}

// Type declaration and functions for sorting the images output
type imagesSorted []imagesTemplateParams

func (a imagesSorted) Len() int      { return len(a) }
func (a imagesSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

type imagesSortedCreated struct{ imagesSorted }

func (a imagesSortedCreated) Less(i, j int) bool {
	return a.imagesSorted[i].CreatedTime.After(a.imagesSorted[j].CreatedTime)
}

type imagesSortedID struct{ imagesSorted }

func (a imagesSortedID) Less(i, j int) bool { return a.imagesSorted[i].ID < a.imagesSorted[j].ID }

type imagesSortedTag struct{ imagesSorted }

func (a imagesSortedTag) Less(i, j int) bool { return a.imagesSorted[i].Tag < a.imagesSorted[j].Tag }

type imagesSortedRepository struct{ imagesSorted }

func (a imagesSortedRepository) Less(i, j int) bool {
	return a.imagesSorted[i].Repository < a.imagesSorted[j].Repository
}

type imagesSortedSize struct{ imagesSorted }

func (a imagesSortedSize) Less(i, j int) bool {
	size1, _ := units.FromHumanSize(a.imagesSorted[i].Size)
	size2, _ := units.FromHumanSize(a.imagesSorted[j].Size)
	return size1 < size2
}

var (
	imagesCommand     cliconfig.ImagesValues
	imagesDescription = "Lists images previously pulled to the system or created on the system."

	_imagesCommand = cobra.Command{
		Use:   "images [flags] [IMAGE]",
		Short: "List images in local storage",
		Long:  imagesDescription,
		RunE: func(cmd *cobra.Command, args []string) error {
			imagesCommand.InputArgs = args
			imagesCommand.GlobalFlags = MainGlobalOpts
			imagesCommand.Remote = remoteclient
			return imagesCmd(&imagesCommand)
		},
		Example: `podman images --format json
  podman images --sort repository --format "table {{.ID}} {{.Repository}} {{.Tag}}"
  podman images --filter dangling=true`,
	}
)

func imagesInit(command *cliconfig.ImagesValues) {
	command.SetHelpTemplate(HelpTemplate())
	command.SetUsageTemplate(UsageTemplate())

	flags := command.Flags()
	flags.BoolVarP(&command.All, "all", "a", false, "Show all images (default hides intermediate images)")
	flags.BoolVar(&command.Digests, "digests", false, "Show digests")
	flags.StringSliceVarP(&command.Filter, "filter", "f", []string{}, "Filter output based on conditions provided (default [])")
	flags.StringVar(&command.Format, "format", "", "Change the output format to JSON or a Go template")
	flags.BoolVarP(&command.Noheading, "noheading", "n", false, "Do not print column headings")
	// TODO Need to learn how to deal with second name being a string instead of a char.
	// This needs to be "no-trunc, notruncate"
	flags.BoolVar(&command.NoTrunc, "no-trunc", false, "Do not truncate output")
	flags.BoolVarP(&command.Quiet, "quiet", "q", false, "Display only image IDs")
	flags.StringVar(&command.Sort, "sort", "created", "Sort by created, id, repository, size, or tag")
	flags.BoolVarP(&command.History, "history", "", false, "Display the image name history")

}

func init() {
	imagesCommand.Command = &_imagesCommand
	imagesInit(&imagesCommand)
}

func imagesCmd(c *cliconfig.ImagesValues) error {
	var (
		image string
	)

	ctx := getContext()
	runtime, err := adapter.GetRuntime(getContext(), &c.PodmanCommand)
	if err != nil {
		return errors.Wrapf(err, "Could not get runtime")
	}
	defer runtime.DeferredShutdown(false)
	if len(c.InputArgs) == 1 {
		image = c.InputArgs[0]
	}
	if len(c.InputArgs) > 1 {
		return errors.New("'podman images' requires at most 1 argument")
	}
	if len(c.Filter) > 0 && image != "" {
		return errors.New("can not specify an image and a filter")
	}
	filters := c.Filter
	if len(filters) < 1 {
		filters = append(filters, fmt.Sprintf("reference=%s", image))
	}

	var sortValues = map[string]bool{
		"created":    true,
		"id":         true,
		"repository": true,
		"size":       true,
		"tag":        true,
	}
	if !sortValues[c.Sort] {
		keys := make([]string, 0, len(sortValues))
		for k := range sortValues {
			keys = append(keys, k)
		}
		return errors.Errorf("invalid sort value %q,  required values: %s", c.Sort, strings.Join(keys, ", "))
	}

	opts := imagesOptions{
		quiet:     c.Quiet,
		noHeading: c.Noheading,
		noTrunc:   c.NoTrunc,
		digests:   c.Digests,
		format:    c.Format,
		sort:      c.Sort,
		all:       c.All,
		history:   c.History,
	}

	opts.outputformat = opts.setOutputFormat()
	filteredImages, err := runtime.GetFilteredImages(filters, false)
	if err != nil {
		return errors.Wrapf(err, "unable to get images")
	}

	for _, image := range filteredImages {
		if image.IsReadOnly() {
			opts.outputformat += "{{.ReadOnly}}\t"
			break
		}
	}
	return generateImagesOutput(ctx, filteredImages, opts)
}

func (i imagesOptions) setOutputFormat() string {
	if i.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"
		return strings.Replace(i.format, `\t`, "\t", -1)
	}
	if i.quiet {
		return formats.IDString
	}
	format := "table {{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}\t"
	if i.noHeading {
		format = "{{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}\t"
	}
	if i.digests {
		format += "{{.Digest}}\t"
	}
	format += "{{.ID}}\t{{.Created}}\t{{.Size}}\t"
	if i.history {
		format += "{{if .History}}{{.History}}{{else}}<none>{{end}}\t"
	}
	return format
}

// imagesToGeneric creates an empty array of interfaces for output
func imagesToGeneric(templParams []imagesTemplateParams, jsonParams []imagesJSONParams) []interface{} {
	genericParams := []interface{}{}
	if len(templParams) > 0 {
		for _, v := range templParams {
			genericParams = append(genericParams, interface{}(v))
		}
		return genericParams
	}
	for _, v := range jsonParams {
		genericParams = append(genericParams, interface{}(v))
	}
	return genericParams
}

func sortImagesOutput(sortBy string, imagesOutput imagesSorted) imagesSorted {
	switch sortBy {
	case "id":
		sort.Sort(imagesSortedID{imagesOutput})
	case "size":
		sort.Sort(imagesSortedSize{imagesOutput})
	case "tag":
		sort.Sort(imagesSortedTag{imagesOutput})
	case "repository":
		sort.Sort(imagesSortedRepository{imagesOutput})
	default:
		// default is created time
		sort.Sort(imagesSortedCreated{imagesOutput})
	}
	return imagesOutput
}

// getImagesTemplateOutput returns the images information to be printed in human readable format
func getImagesTemplateOutput(ctx context.Context, images []*adapter.ContainerImage, opts imagesOptions) imagesSorted {
	var imagesOutput imagesSorted
	for _, img := range images {
		// If all is false and the image doesn't have a name, check to see if the top layer of the image is a parent
		// to another image's top layer. If it is, then it is an intermediate image so don't print out if the --all flag
		// is not set.
		isParent, err := img.IsParent(ctx)
		if err != nil {
			logrus.Errorf("error checking if image is a parent %q: %v", img.ID(), err)
		}
		if !opts.all && len(img.Names()) == 0 && isParent {
			continue
		}
		createdTime := img.Created()

		imageID := "sha256:" + img.ID()
		if !opts.noTrunc {
			imageID = shortID(img.ID())
		}

		// get all specified repo:tag and repo@digest pairs and print them separately
		repopairs, err := image.ReposToMap(img.Names())
		if err != nil {
			logrus.Errorf("error finding tag/digest for %s", img.ID())
		}
	outer:
		for repo, tags := range repopairs {
			for _, tag := range tags {
				size, err := img.Size(ctx)
				var sizeStr string
				if err != nil {
					sizeStr = err.Error()
				} else {
					sizeStr = units.HumanSizeWithPrecision(float64(*size), 3)
					lastNumIdx := strings.LastIndexFunc(sizeStr, unicode.IsNumber)
					sizeStr = sizeStr[:lastNumIdx+1] + " " + sizeStr[lastNumIdx+1:]
				}
				var imageDigest digest.Digest
				if len(tag) == 71 && strings.HasPrefix(tag, "sha256:") {
					imageDigest = digest.Digest(tag)
					tag = ""
				} else if img.Digest() != "" {
					imageDigest = img.Digest()
				}
				params := imagesTemplateParams{
					Repository:  repo,
					Tag:         tag,
					ID:          imageID,
					Digest:      imageDigest,
					Digests:     img.Digests(),
					CreatedTime: createdTime,
					Created:     units.HumanDuration(time.Since(createdTime)) + " ago",
					Size:        sizeStr,
					ReadOnly:    img.IsReadOnly(),
					History:     strings.Join(img.NamesHistory(), ", "),
				}
				imagesOutput = append(imagesOutput, params)
				if opts.quiet { // Show only one image ID when quiet
					break outer
				}
			}
		}
	}

	// Sort images by created time
	sortImagesOutput(opts.sort, imagesOutput)
	return imagesOutput
}

// getImagesJSONOutput returns the images information in its raw form
func getImagesJSONOutput(ctx context.Context, images []*adapter.ContainerImage) []imagesJSONParams {
	imagesOutput := []imagesJSONParams{}
	for _, img := range images {
		size, err := img.Size(ctx)
		if err != nil {
			size = nil
		}
		params := imagesJSONParams{
			ID:       img.ID(),
			Name:     img.Names(),
			Digest:   img.Digest(),
			Digests:  img.Digests(),
			Created:  img.Created(),
			Size:     size,
			ReadOnly: img.IsReadOnly(),
			History:  img.NamesHistory(),
		}
		imagesOutput = append(imagesOutput, params)
	}
	return imagesOutput
}

// generateImagesOutput generates the images based on the format provided

func generateImagesOutput(ctx context.Context, images []*adapter.ContainerImage, opts imagesOptions) error {
	templateMap := GenImageOutputMap()
	var out formats.Writer

	switch opts.format {
	case formats.JSONString:
		imagesOutput := getImagesJSONOutput(ctx, images)
		out = formats.JSONStructArray{Output: imagesToGeneric([]imagesTemplateParams{}, imagesOutput)}
	default:
		imagesOutput := getImagesTemplateOutput(ctx, images, opts)
		out = formats.StdoutTemplateArray{Output: imagesToGeneric(imagesOutput, []imagesJSONParams{}), Template: opts.outputformat, Fields: templateMap}
	}
	return out.Out()
}

// GenImageOutputMap generates the map used for outputting the images header
// without requiring a populated image. This replaces the previous HeaderMap
// call.
func GenImageOutputMap() map[string]string {
	io := imagesTemplateParams{}
	v := reflect.Indirect(reflect.ValueOf(io))
	values := make(map[string]string)

	for i := 0; i < v.NumField(); i++ {
		key := v.Type().Field(i).Name
		value := key
		if value == "ID" {
			value = "Image" + value
		}

		if value == "ReadOnly" {
			values[key] = "R/O"
			continue
		}
		values[key] = strings.ToUpper(splitCamelCase(value))
	}
	return values
}
