package container

import (
	"strconv"
	"strings"
	"sync"

	"github.com/docker/cli/cli/command/formatter"
	"github.com/docker/go-units"
)

const (
	winOSType                  = "windows"
	defaultStatsTableFormat    = "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"
	winDefaultStatsTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"

	containerHeader = "CONTAINER"
	cpuPercHeader   = "CPU %"
	netIOHeader     = "NET I/O"
	blockIOHeader   = "BLOCK I/O"
	memPercHeader   = "MEM %"             // Used only on Linux
	winMemUseHeader = "PRIV WORKING SET"  // Used only on Windows
	memUseHeader    = "MEM USAGE / LIMIT" // Used only on Linux
	pidsHeader      = "PIDS"              // Used only on Linux

	noValue = "--"
)

// StatsEntry represents the statistics data collected from a container
type StatsEntry struct {
	Container        string
	Name             string
	ID               string
	CPUPercentage    float64
	Memory           float64 // On Windows this is the private working set
	MemoryLimit      float64 // Not used on Windows
	MemoryPercentage float64 // Not used on Windows
	NetworkRx        float64
	NetworkTx        float64
	BlockRead        float64
	BlockWrite       float64
	PidsCurrent      uint64 // Not used on Windows
	IsInvalid        bool
}

// Stats represents an entity to store containers statistics synchronously
type Stats struct {
	mutex sync.RWMutex
	StatsEntry
	err error
}

// GetError returns the container statistics error.
// This is used to determine whether the statistics are valid or not
func (cs *Stats) GetError() error {
	cs.mutex.RLock()
	defer cs.mutex.RUnlock()
	return cs.err
}

// SetErrorAndReset zeroes all the container statistics and store the error.
// It is used when receiving time out error during statistics collecting to reduce lock overhead
func (cs *Stats) SetErrorAndReset(err error) {
	cs.mutex.Lock()
	defer cs.mutex.Unlock()
	cs.CPUPercentage = 0
	cs.Memory = 0
	cs.MemoryPercentage = 0
	cs.MemoryLimit = 0
	cs.NetworkRx = 0
	cs.NetworkTx = 0
	cs.BlockRead = 0
	cs.BlockWrite = 0
	cs.PidsCurrent = 0
	cs.err = err
	cs.IsInvalid = true
}

// SetError sets container statistics error
func (cs *Stats) SetError(err error) {
	cs.mutex.Lock()
	defer cs.mutex.Unlock()
	cs.err = err
	if err != nil {
		cs.IsInvalid = true
	}
}

// SetStatistics set the container statistics
func (cs *Stats) SetStatistics(s StatsEntry) {
	cs.mutex.Lock()
	defer cs.mutex.Unlock()
	s.Container = cs.Container
	cs.StatsEntry = s
}

// GetStatistics returns container statistics with other meta data such as the container name
func (cs *Stats) GetStatistics() StatsEntry {
	cs.mutex.RLock()
	defer cs.mutex.RUnlock()
	return cs.StatsEntry
}

// NewStatsFormat returns a format for rendering an CStatsContext
func NewStatsFormat(source, osType string) formatter.Format {
	if source == formatter.TableFormatKey {
		if osType == winOSType {
			return winDefaultStatsTableFormat
		}
		return defaultStatsTableFormat
	}
	return formatter.Format(source)
}

// NewStats returns a new Stats entity using the given ID, ID-prefix, or
// name to resolve the container.
func NewStats(idOrName string) *Stats {
	// FIXME(thaJeztah): "idOrName" is used for fuzzy-matching the container, which can result in multiple stats for the same container.
	// We should resolve the canonical ID once, then use that as reference
	// to prevent duplicates. Various parts in the code compare Container
	// against "ID" only (not considering "name" or "ID-prefix").
	return &Stats{StatsEntry: StatsEntry{Container: idOrName}}
}

// statsFormatWrite renders the context for a list of containers statistics
func statsFormatWrite(ctx formatter.Context, stats []StatsEntry, osType string, trunc bool) error {
	// TODO(thaJeztah): this should be taken from the (first) StatsEntry instead.
	// also, assuming all stats are for the same platform (and basing the
	// column headers on that) won't allow aggregated results, which could
	// be mixed platform.
	memUsage := memUseHeader
	if osType == winOSType {
		memUsage = winMemUseHeader
	}
	statsCtx := statsContext{os: osType}
	statsCtx.Header = formatter.SubHeaderContext{
		"Container": containerHeader,
		"Name":      formatter.NameHeader,
		"ID":        formatter.ContainerIDHeader,
		"CPUPerc":   cpuPercHeader,
		"MemUsage":  memUsage,
		"MemPerc":   memPercHeader,
		"NetIO":     netIOHeader,
		"BlockIO":   blockIOHeader,
		"PIDs":      pidsHeader,
	}
	return ctx.Write(&statsCtx, func(format func(subContext formatter.SubContext) error) error {
		for _, cstats := range stats {
			if err := format(&statsContext{
				s:     cstats,
				os:    osType,
				trunc: trunc,
			}); err != nil {
				return err
			}
		}
		return nil
	})
}

type statsContext struct {
	formatter.HeaderContext
	s     StatsEntry
	os    string
	trunc bool
}

func (c *statsContext) MarshalJSON() ([]byte, error) {
	return formatter.MarshalJSON(c)
}

func (c *statsContext) Container() string {
	return c.s.Container
}

func (c *statsContext) Name() string {
	// Trim the "/" prefix (if present).
	if name := strings.TrimPrefix(c.s.Name, "/"); name != "" {
		return name
	}
	return noValue
}

func (c *statsContext) ID() string {
	if c.trunc {
		return formatter.TruncateID(c.s.ID)
	}
	return c.s.ID
}

func (c *statsContext) CPUPerc() string {
	if c.s.IsInvalid {
		return noValue
	}
	return formatPercentage(c.s.CPUPercentage)
}

func (c *statsContext) MemUsage() string {
	if c.s.IsInvalid {
		return "-- / --"
	}
	if c.os == winOSType {
		return units.BytesSize(c.s.Memory)
	}
	return units.BytesSize(c.s.Memory) + " / " + units.BytesSize(c.s.MemoryLimit)
}

func (c *statsContext) MemPerc() string {
	if c.s.IsInvalid || c.os == winOSType {
		return noValue
	}
	return formatPercentage(c.s.MemoryPercentage)
}

func (c *statsContext) NetIO() string {
	if c.s.IsInvalid {
		return noValue
	}
	return units.HumanSizeWithPrecision(c.s.NetworkRx, 3) + " / " + units.HumanSizeWithPrecision(c.s.NetworkTx, 3)
}

func (c *statsContext) BlockIO() string {
	if c.s.IsInvalid {
		return noValue
	}
	return units.HumanSizeWithPrecision(c.s.BlockRead, 3) + " / " + units.HumanSizeWithPrecision(c.s.BlockWrite, 3)
}

func (c *statsContext) PIDs() string {
	if c.s.IsInvalid || c.os == winOSType {
		return noValue
	}
	return strconv.FormatUint(c.s.PidsCurrent, 10)
}

func formatPercentage(val float64) string {
	return strconv.FormatFloat(val, 'f', 2, 64) + "%"
}
