// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package cpu

import (
	"strconv"

	"github.com/elastic/beats/libbeat/common"
	"github.com/elastic/beats/libbeat/logp"
	"github.com/elastic/beats/metricbeat/module/docker"
)

type CPUStats struct {
	Time                        common.Time
	Container                   *docker.Container
	PerCpuUsage                 common.MapStr
	TotalUsage                  float64
	UsageInKernelmode           uint64
	UsageInKernelmodePercentage float64
	UsageInUsermode             uint64
	UsageInUsermodePercentage   float64
	SystemUsage                 uint64
	SystemUsagePercentage       float64
}

// CPUService is a helper to collect docker CPU metrics
type CPUService struct {
	Cores bool
}

func NewCpuService() *CPUService {
	return &CPUService{}
}

func (c *CPUService) getCPUStatsList(rawStats []docker.Stat, dedot bool) []CPUStats {
	formattedStats := []CPUStats{}

	for _, stats := range rawStats {
		formattedStats = append(formattedStats, c.getCPUStats(&stats, dedot))
	}

	return formattedStats
}

func (c *CPUService) getCPUStats(myRawStat *docker.Stat, dedot bool) CPUStats {
	usage := cpuUsage{Stat: myRawStat}

	stats := CPUStats{
		Time:                        common.Time(myRawStat.Stats.Read),
		Container:                   docker.NewContainer(myRawStat.Container, dedot),
		TotalUsage:                  usage.Total(),
		UsageInKernelmode:           myRawStat.Stats.CPUStats.CPUUsage.UsageInKernelmode,
		UsageInKernelmodePercentage: usage.InKernelMode(),
		UsageInUsermode:             myRawStat.Stats.CPUStats.CPUUsage.UsageInUsermode,
		UsageInUsermodePercentage:   usage.InUserMode(),
		SystemUsage:                 myRawStat.Stats.CPUStats.SystemUsage,
		SystemUsagePercentage:       usage.System(),
	}

	if c.Cores {
		stats.PerCpuUsage = usage.PerCPU()
	}

	return stats
}

// TODO: These helper should be merged with the cpu helper in system/cpu

type cpuUsage struct {
	*docker.Stat

	cpus        int
	systemDelta uint64
}

func (u *cpuUsage) CPUs() int {
	if u.cpus == 0 {
		u.cpus = len(u.Stats.CPUStats.CPUUsage.PercpuUsage)
	}
	return u.cpus
}

func (u *cpuUsage) SystemDelta() uint64 {
	if u.systemDelta == 0 {
		u.systemDelta = u.Stats.CPUStats.SystemUsage - u.Stats.PreCPUStats.SystemUsage
	}
	return u.systemDelta
}

func (u *cpuUsage) PerCPU() common.MapStr {
	var output common.MapStr
	if len(u.Stats.CPUStats.CPUUsage.PercpuUsage) == len(u.Stats.PreCPUStats.CPUUsage.PercpuUsage) {
		output = common.MapStr{}
		for index := range u.Stats.CPUStats.CPUUsage.PercpuUsage {
			cpu := common.MapStr{}
			cpu["pct"] = u.calculatePercentage(
				u.Stats.CPUStats.CPUUsage.PercpuUsage[index],
				u.Stats.PreCPUStats.CPUUsage.PercpuUsage[index])
			cpu["ticks"] = u.Stats.CPUStats.CPUUsage.PercpuUsage[index]
			output[strconv.Itoa(index)] = cpu
		}
	}
	return output
}

func (u *cpuUsage) Total() float64 {
	return u.calculatePercentage(u.Stats.CPUStats.CPUUsage.TotalUsage, u.Stats.PreCPUStats.CPUUsage.TotalUsage)
}

func (u *cpuUsage) InKernelMode() float64 {
	return u.calculatePercentage(u.Stats.CPUStats.CPUUsage.UsageInKernelmode, u.Stats.PreCPUStats.CPUUsage.UsageInKernelmode)
}

func (u *cpuUsage) InUserMode() float64 {
	return u.calculatePercentage(u.Stats.CPUStats.CPUUsage.UsageInUsermode, u.Stats.PreCPUStats.CPUUsage.UsageInUsermode)
}

func (u *cpuUsage) System() float64 {
	return u.calculatePercentage(u.Stats.CPUStats.SystemUsage, u.Stats.PreCPUStats.SystemUsage)
}

// This function is meant to calculate the % CPU time change between two successive readings.
// The "oldValue" refers to the CPU statistics of the last read.
// Time here is expressed by second and not by nanoseconde.
// The main goal is to expose the %, in the same way, it's displayed by docker Client.
func (u *cpuUsage) calculatePercentage(newValue uint64, oldValue uint64) float64 {
	if newValue < oldValue {
		logp.Err("Error calculating CPU time change for docker module: new stats value (%v) is lower than the old one(%v)", newValue, oldValue)
		return -1
	}
	value := newValue - oldValue
	if value == 0 || u.SystemDelta() == 0 {
		return 0
	}

	return float64(uint64(u.CPUs())*value) / float64(u.SystemDelta())
}
