package main

import (
	"fmt"
	"html/template"
	"os"
	"reflect"
	"sort"
	"strings"
	"text/tabwriter"
	"time"

	tm "github.com/buger/goterm"
	"github.com/containers/buildah/pkg/formats"
	"github.com/containers/libpod/cmd/podman/cliconfig"
	"github.com/containers/libpod/cmd/podman/shared"
	"github.com/containers/libpod/pkg/adapter"
	"github.com/docker/go-units"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
)

const (
	hid      = "CONTAINER ID"
	himage   = "IMAGE"
	hcommand = "COMMAND"
	hcreated = "CREATED"
	hstatus  = "STATUS"
	hports   = "PORTS"
	hnames   = "NAMES"
	hsize    = "SIZE"
	hinfra   = "IS INFRA" //nolint
	hpod     = "POD"
	nspid    = "PID"
	nscgroup = "CGROUPNS"
	nsipc    = "IPC"
	nsmnt    = "MNT"
	nsnet    = "NET"
	nspidns  = "PIDNS"
	nsuserns = "USERNS"
	nsuts    = "UTS"
)

// Type declaration and functions for sorting the PS output
type psSorted []shared.PsContainerOutput

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

type psSortedCommand struct{ psSorted }

func (a psSortedCommand) Less(i, j int) bool {
	return a.psSorted[i].Command < a.psSorted[j].Command
}

type psSortedCreated struct{ psSorted }

func (a psSortedCreated) Less(i, j int) bool {
	return a.psSorted[i].CreatedAt.After(a.psSorted[j].CreatedAt)
}

type psSortedId struct{ psSorted }

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

type psSortedImage struct{ psSorted }

func (a psSortedImage) Less(i, j int) bool { return a.psSorted[i].Image < a.psSorted[j].Image }

type psSortedNames struct{ psSorted }

func (a psSortedNames) Less(i, j int) bool { return a.psSorted[i].Names < a.psSorted[j].Names }

type psSortedPod struct{ psSorted }

func (a psSortedPod) Less(i, j int) bool { return a.psSorted[i].Pod < a.psSorted[j].Pod }

type psSortedRunningFor struct{ psSorted }

func (a psSortedRunningFor) Less(i, j int) bool {
	return a.psSorted[j].StartedAt.After(a.psSorted[i].StartedAt)
}

type psSortedStatus struct{ psSorted }

func (a psSortedStatus) Less(i, j int) bool { return a.psSorted[i].Status < a.psSorted[j].Status }

type psSortedSize struct{ psSorted }

func (a psSortedSize) Less(i, j int) bool {
	if a.psSorted[i].Size == nil || a.psSorted[j].Size == nil {
		return false
	}
	return a.psSorted[i].Size.RootFsSize < a.psSorted[j].Size.RootFsSize
}

var (
	psCommand     cliconfig.PsValues
	psDescription = "Prints out information about the containers"
	_psCommand    = cobra.Command{
		Use:   "ps",
		Args:  noSubArgs,
		Short: "List containers",
		Long:  psDescription,
		RunE: func(cmd *cobra.Command, args []string) error {
			psCommand.InputArgs = args
			psCommand.GlobalFlags = MainGlobalOpts
			psCommand.Remote = remoteclient
			return psCmd(&psCommand)
		},
		Example: `podman ps -a
  podman ps -a --format "{{.ID}}  {{.Image}}  {{.Labels}}  {{.Mounts}}"
  podman ps --size --sort names`,
	}
)

func psInit(command *cliconfig.PsValues) {
	command.SetHelpTemplate(HelpTemplate())
	command.SetUsageTemplate(UsageTemplate())
	flags := command.Flags()
	flags.BoolVarP(&command.All, "all", "a", false, "Show all the containers, default is only running containers")
	flags.StringSliceVarP(&command.Filter, "filter", "f", []string{}, "Filter output based on conditions given")
	flags.StringVar(&command.Format, "format", "", "Pretty-print containers to JSON or using a Go template")
	flags.IntVarP(&command.Last, "last", "n", -1, "Print the n last created containers (all states)")
	flags.BoolVarP(&command.Latest, "latest", "l", false, "Show the latest container created (all states)")
	flags.BoolVar(&command.Namespace, "namespace", false, "Display namespace information")
	flags.BoolVar(&command.Namespace, "ns", false, "Display namespace information")
	flags.BoolVar(&command.NoTrunct, "no-trunc", false, "Display the extended information")
	flags.BoolVarP(&command.Pod, "pod", "p", false, "Print the ID and name of the pod the containers are associated with")
	flags.BoolVarP(&command.Quiet, "quiet", "q", false, "Print the numeric IDs of the containers only")
	flags.BoolVarP(&command.Size, "size", "s", false, "Display the total file sizes")
	flags.StringVar(&command.Sort, "sort", "created", "Sort output by command, created, id, image, names, runningfor, size, or status")
	flags.BoolVar(&command.Sync, "sync", false, "Sync container state with OCI runtime")
	flags.UintVarP(&command.Watch, "watch", "w", 0, "Watch the ps output on an interval in seconds")

	markFlagHiddenForRemoteClient("latest", flags)
}

func init() {
	psCommand.Command = &_psCommand
	psInit(&psCommand)
}

func psCmd(c *cliconfig.PsValues) error {
	var (
		watch   bool
		runtime *adapter.LocalRuntime
		err     error
	)

	if c.Watch > 0 {
		watch = true
	}

	if c.Watch > 0 && c.Latest {
		return errors.New("the watch and latest flags cannot be used together")
	}

	if err := checkFlagsPassed(c); err != nil {
		return errors.Wrapf(err, "error with flags passed")
	}
	if !c.Size {
		runtime, err = adapter.GetRuntimeNoStore(getContext(), &c.PodmanCommand)
	} else {
		runtime, err = adapter.GetRuntime(getContext(), &c.PodmanCommand)
	}
	if err != nil {
		return errors.Wrapf(err, "error creating libpod runtime")
	}

	defer runtime.DeferredShutdown(false)

	if !watch {
		if err := psDisplay(c, runtime); err != nil {
			return err
		}
	} else {
		for {
			tm.Clear()
			tm.MoveCursor(1, 1)
			tm.Flush()
			if err := psDisplay(c, runtime); err != nil {
				return err
			}
			time.Sleep(time.Duration(c.Watch) * time.Second)
			tm.Clear()
			tm.MoveCursor(1, 1)
			tm.Flush()
		}
	}
	return nil
}

func printQuiet(containers []shared.PsContainerOutput) error {
	for _, c := range containers {
		fmt.Println(c.ID)
	}
	return nil
}

// checkFlagsPassed checks if mutually exclusive flags are passed together
func checkFlagsPassed(c *cliconfig.PsValues) error {
	// latest, and last are mutually exclusive.
	if c.Last >= 0 && c.Latest {
		return errors.Errorf("last and latest are mutually exclusive")
	}
	// Quiet conflicts with size, namespace, and format with a Go template
	if c.Quiet {
		if c.Size || c.Namespace || (c.Flag("format").Changed &&
			c.Format != formats.JSONString) {
			return errors.Errorf("quiet conflicts with size, namespace, and format with go template")
		}
	}
	// Size and namespace conflict with each other
	if c.Size && c.Namespace {
		return errors.Errorf("size and namespace options conflict")
	}
	return nil
}

func sortPsOutput(sortBy string, psOutput psSorted) (psSorted, error) {
	switch sortBy {
	case "id":
		sort.Sort(psSortedId{psOutput})
	case "image":
		sort.Sort(psSortedImage{psOutput})
	case "command":
		sort.Sort(psSortedCommand{psOutput})
	case "runningfor":
		sort.Sort(psSortedRunningFor{psOutput})
	case "status":
		sort.Sort(psSortedStatus{psOutput})
	case "size":
		sort.Sort(psSortedSize{psOutput})
	case "names":
		sort.Sort(psSortedNames{psOutput})
	case "created":
		sort.Sort(psSortedCreated{psOutput})
	case "pod":
		sort.Sort(psSortedPod{psOutput})
	default:
		return nil, errors.Errorf("invalid option for --sort, options are: command, created, id, image, names, runningfor, size, or status")
	}
	return psOutput, nil
}

func printFormat(format string, containers []shared.PsContainerOutput) error {
	// return immediately if no containers are present
	if len(containers) == 0 {
		return nil
	}

	// Use a tabwriter to align column format
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)

	// Make a map of the field names for the headers
	headerNames := make(map[string]string)
	v := reflect.ValueOf(containers[0])
	t := v.Type()
	for i := 0; i < t.NumField(); i++ {
		headerNames[t.Field(i).Name] = t.Field(i).Name
	}

	// Spit out the header if "table" is present in the format
	if strings.HasPrefix(format, "table") {
		hformat := strings.Replace(strings.TrimSpace(format[5:]), " ", "\t", -1)
		format = hformat
		headerTmpl, err := template.New("header").Parse(hformat)
		if err != nil {
			return err
		}
		if err := headerTmpl.Execute(w, headerNames); err != nil {
			return err
		}
		fmt.Fprintln(w, "")
	}

	// Spit out the data rows now
	dataTmpl, err := template.New("data").Parse(format)
	if err != nil {
		return err
	}

	for _, container := range containers {
		if err := dataTmpl.Execute(w, container); err != nil {
			return err
		}
		fmt.Fprintln(w, "")
	}
	// Flush the writer
	return w.Flush()
}

func dumpJSON(containers []shared.PsContainerOutput) error {
	b, err := json.MarshalIndent(containers, "", "     ")
	if err != nil {
		return err
	}
	os.Stdout.Write(b)
	return nil
}

func psDisplay(c *cliconfig.PsValues, runtime *adapter.LocalRuntime) error {
	var (
		err error
	)
	opts := shared.PsOptions{
		All:       c.All,
		Format:    c.Format,
		Last:      c.Last,
		Latest:    c.Latest,
		NoTrunc:   c.NoTrunct,
		Pod:       c.Pod,
		Quiet:     c.Quiet,
		Size:      c.Size,
		Namespace: c.Namespace,
		Sort:      c.Sort,
		Sync:      c.Sync,
	}

	pss, err := runtime.Ps(c, opts)
	if err != nil {
		return err
	}
	// Here and down
	if opts.Sort != "" {
		pss, err = sortPsOutput(opts.Sort, pss)
		if err != nil {
			return err
		}
	}

	// If quiet, print only cids and return
	if opts.Quiet {
		return printQuiet(pss)
	}

	// If the user wants their own GO template format
	if opts.Format != "" {
		if opts.Format == "json" {
			return dumpJSON(pss)
		}
		return printFormat(opts.Format, pss)
	}

	// Define a tab writer with stdout as the output
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)

	// Output standard PS headers
	if !opts.Namespace {
		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s", hid, himage, hcommand, hcreated, hstatus, hports, hnames)
		// User wants pod info
		if opts.Pod {
			fmt.Fprintf(w, "\t%s", hpod)
		}
		//User wants size info
		if opts.Size {
			fmt.Fprintf(w, "\t%s", hsize)
		}
	} else {
		// Output Namespace headers
		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s", hid, hnames, nspid, nscgroup, nsipc, nsmnt, nsnet, nspidns, nsuserns, nsuts)
	}

	// Now iterate each container and output its information
	for _, container := range pss {

		// Standard PS output
		if !opts.Namespace {
			fmt.Fprintf(w, "\n%s\t%s\t%s\t%s\t%s\t%s\t%s", container.ID, container.Image, container.Command, container.Created, container.Status, container.Ports, container.Names)
			// User wants pod info
			if opts.Pod {
				fmt.Fprintf(w, "\t%s", container.Pod)
			}
			//User wants size info
			if opts.Size {
				var size string
				if container.Size == nil {
					size = units.HumanSizeWithPrecision(0, 0)
				} else {
					size = units.HumanSizeWithPrecision(float64(container.Size.RwSize), 3) + " (virtual " + units.HumanSizeWithPrecision(float64(container.Size.RootFsSize), 3) + ")"
				}
				fmt.Fprintf(w, "\t%s", size)
			}

		} else {
			// Print namespace information
			ns := runtime.GetNamespaces(container)
			fmt.Fprintf(w, "\n%s\t%s\t%d\t%s\t%s\t%s\t%s\t%s\t%s\t%s", container.ID, container.Names, container.Pid, ns.Cgroup, ns.IPC, ns.MNT, ns.NET, ns.PIDNS, ns.User, ns.UTS)
		}

	}
	fmt.Fprint(w, "\n")
	return w.Flush()
}
