package libpod

import (
	"io/ioutil"
	"path/filepath"
	"strings"

	"github.com/pkg/errors"
	"github.com/projectatomic/libpod/pkg/util"
	"github.com/projectatomic/libpod/utils"
	"github.com/sirupsen/logrus"
)

// GetContainerPids reads sysfs to obtain the pids associated with the container's cgroup
// and uses locking
func (c *Container) GetContainerPids() ([]string, error) {
	if !c.batched {
		c.lock.Lock()
		defer c.lock.Unlock()
		if err := c.syncContainer(); err != nil {
			return []string{}, errors.Wrapf(err, "error updating container %s state", c.ID())
		}
	}
	return c.getContainerPids()
}

// Gets the pids for a container without locking.  should only be called from a func where
// locking has already been established.
func (c *Container) getContainerPids() ([]string, error) {
	cgroupPath, err := c.CGroupPath()
	if err != nil {
		return nil, err
	}

	taskFile := filepath.Join("/sys/fs/cgroup/pids", cgroupPath, "tasks")

	logrus.Debug("reading pids from ", taskFile)

	content, err := ioutil.ReadFile(taskFile)
	if err != nil {
		return []string{}, errors.Wrapf(err, "unable to read pids from %s", taskFile)
	}
	return strings.Fields(string(content)), nil
}

// GetContainerPidInformation calls ps with the appropriate options and returns
// the results as a string and the container's PIDs as a []string.  Note that
// the ps output columns of each string are separated by a '\t\'.  Currently,
// the args argument is overwriten with []string{"-ef"} until there is a
// portable library for ps-1 or to parse the procFS to extract all data.
func (c *Container) GetContainerPidInformation(args []string) ([]string, error) {
	// XXX(ps-issue): args is overwriten with []{"-ef"} as the ps-1 tool
	// doesn't support a generic way of splitting columns, rendering its
	// output hard to parse.  Docker first deterimes the number of columns
	// and merges all exceeding ones into the last one.  We believe that
	// writing a go library which parses procFS in a ps-compatible way may
	// be more beneficial in the long run.  Until then, args will be
	// ignored.
	args = []string{"-ef"}

	if !c.batched {
		c.lock.Lock()
		defer c.lock.Unlock()
		if err := c.syncContainer(); err != nil {
			return []string{}, errors.Wrapf(err, "error updating container %s state", c.ID())
		}
	}
	pids, err := c.getContainerPids()
	if err != nil {
		return []string{}, errors.Wrapf(err, "unable to obtain pids for ", c.ID())
	}
	args = append(args, "-p", strings.Join(pids, ","))
	logrus.Debug("Executing: ", strings.Join(args, " "))
	results, err := utils.ExecCmd("ps", args...)
	if err != nil {
		return []string{}, errors.Wrapf(err, "unable to obtain information about pids")
	}

	filteredOutput, err := filterPids(results, pids)
	if err != nil {
		return []string{}, err
	}
	return filteredOutput, nil
}

func filterPids(psOutput string, pids []string) ([]string, error) {
	var output []string
	results := strings.Split(psOutput, "\n")
	// The headers are in the first line of the results
	headers := fieldsASCII(results[0])
	// We need to make sure PID in headers, so that we can filter
	// Pids that don't belong.

	// append the headers back in
	output = append(output, strings.Join(headers, "\t"))

	pidIndex := -1
	for i, header := range headers {
		if header == "PID" {
			pidIndex = i
		}
	}
	if pidIndex == -1 {
		return []string{}, errors.Errorf("unable to find PID field in ps output. try a different set of ps arguments")
	}
	for _, l := range results[1:] {
		if l == "" {
			continue
		}
		cols := fieldsASCII(l)
		pid := cols[pidIndex]
		if util.StringInSlice(pid, pids) {
			// XXX(ps-issue): Strip cols down to the header's size
			// and merge exceeding fields. This is required to
			// "merge" the overhanging CMD entries which can
			// contain white spaces.
			out := cols[:len(headers)-1]
			out = append(out, strings.Join(cols[len(headers)-1:], " "))
			output = append(output, strings.Join(out, "\t"))
		}
	}
	return output, nil
}

// Detects ascii whitespaces
func fieldsASCII(s string) []string {
	fn := func(r rune) bool {
		switch r {
		case '\t', '\n', '\f', '\r', ' ':
			return true
		}
		return false
	}
	return strings.FieldsFunc(s, fn)
}