// Copyright 2019 The Prometheus Authors
// Licensed 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.

//go:build !nopowersupplyclass

package collector

/*
#cgo LDFLAGS: -framework IOKit -framework CoreFoundation
#include <CoreFoundation/CFNumber.h>
#include <CoreFoundation/CFRunLoop.h>
#include <CoreFoundation/CFString.h>
#include <IOKit/ps/IOPowerSources.h>
#include <IOKit/ps/IOPSKeys.h>

// values collected from IOKit Power Source APIs
// Functions documentation available at
// https://developer.apple.com/documentation/iokit/iopowersources_h
// CFDictionary keys definition
// https://developer.apple.com/documentation/iokit/iopskeys_h/defines
struct macos_powersupply {
   char *Name;
   char *PowerSourceState;
   char *Type;
   char *TransportType;
   char *BatteryHealth;
   char *HardwareSerialNumber;

   int *PowerSourceID;
   int *CurrentCapacity;
   int *MaxCapacity;
   int *DesignCapacity;
   int *NominalCapacity;

   int *TimeToEmpty;
   int *TimeToFullCharge;

   int *Voltage;
   int *Current;

   int *Temperature;

   // boolean values
   int *IsCharged;
   int *IsCharging;
   int *InternalFailure;
   int *IsPresent;
};

int *CFDictionaryGetInt(CFDictionaryRef theDict, const void *key) {
    CFNumberRef tmp;
    int *value;

    tmp = CFDictionaryGetValue(theDict, key);

    if (tmp == NULL)
        return NULL;

    value = (int*)malloc(sizeof(int));
    if (CFNumberGetValue(tmp, kCFNumberIntType, value)) {
        return value;
    }

    free(value);
    return NULL;
}

int *CFDictionaryGetBoolean(CFDictionaryRef theDict, const void *key) {
    CFBooleanRef tmp;
    int *value;

    tmp = CFDictionaryGetValue(theDict, key);

    if (tmp == NULL)
        return NULL;

    value = (int*)malloc(sizeof(int));
    if (CFBooleanGetValue(tmp)) {
        *value = 1;
    } else {
        *value = 0;
    }

    return value;
}

char *CFDictionaryGetSring(CFDictionaryRef theDict, const void *key) {
    CFStringRef tmp;
    CFIndex size;
    char *value;

    tmp = CFDictionaryGetValue(theDict, key);

    if (tmp == NULL)
        return NULL;

    size = CFStringGetLength(tmp) + 1;
    value = (char*)malloc(size);

    if(CFStringGetCString(tmp, value, size, kCFStringEncodingUTF8)) {
         return value;
    }

    free(value);
    return NULL;
}

struct macos_powersupply* getPowerSupplyInfo(CFDictionaryRef powerSourceInformation) {
    struct macos_powersupply *ret;

    if (powerSourceInformation == NULL)
        return NULL;

    ret = (struct macos_powersupply*)malloc(sizeof(struct macos_powersupply));

    ret->PowerSourceID    = CFDictionaryGetInt(powerSourceInformation, CFSTR(kIOPSPowerSourceIDKey));
    ret->CurrentCapacity  = CFDictionaryGetInt(powerSourceInformation, CFSTR(kIOPSCurrentCapacityKey));
    ret->MaxCapacity      = CFDictionaryGetInt(powerSourceInformation, CFSTR(kIOPSMaxCapacityKey));
    ret->DesignCapacity   = CFDictionaryGetInt(powerSourceInformation, CFSTR(kIOPSDesignCapacityKey));
    ret->NominalCapacity  = CFDictionaryGetInt(powerSourceInformation, CFSTR(kIOPSNominalCapacityKey));
    ret->TimeToEmpty      = CFDictionaryGetInt(powerSourceInformation, CFSTR(kIOPSTimeToEmptyKey));
    ret->TimeToFullCharge = CFDictionaryGetInt(powerSourceInformation, CFSTR(kIOPSTimeToFullChargeKey));
    ret->Voltage          = CFDictionaryGetInt(powerSourceInformation, CFSTR(kIOPSVoltageKey));
    ret->Current          = CFDictionaryGetInt(powerSourceInformation, CFSTR(kIOPSCurrentKey));
    ret->Temperature      = CFDictionaryGetInt(powerSourceInformation, CFSTR(kIOPSTemperatureKey));

    ret->Name = CFDictionaryGetSring(powerSourceInformation, CFSTR(kIOPSNameKey));
    ret->PowerSourceState = CFDictionaryGetSring(powerSourceInformation, CFSTR(kIOPSPowerSourceStateKey));
    ret->Type = CFDictionaryGetSring(powerSourceInformation, CFSTR(kIOPSTypeKey));
    ret->TransportType = CFDictionaryGetSring(powerSourceInformation, CFSTR(kIOPSTransportTypeKey));
    ret->BatteryHealth = CFDictionaryGetSring(powerSourceInformation, CFSTR(kIOPSBatteryHealthKey));
    ret->HardwareSerialNumber = CFDictionaryGetSring(powerSourceInformation, CFSTR(kIOPSHardwareSerialNumberKey));

    ret->IsCharged       = CFDictionaryGetBoolean(powerSourceInformation, CFSTR(kIOPSIsChargedKey));
    ret->IsCharging      = CFDictionaryGetBoolean(powerSourceInformation, CFSTR(kIOPSIsChargingKey));
    ret->InternalFailure = CFDictionaryGetBoolean(powerSourceInformation, CFSTR(kIOPSInternalFailureKey));
    ret->IsPresent       = CFDictionaryGetBoolean(powerSourceInformation, CFSTR(kIOPSIsPresentKey));

    return ret;
}



void releasePowerSupply(struct macos_powersupply *ps) {
    free(ps->Name);
    free(ps->PowerSourceState);
    free(ps->Type);
    free(ps->TransportType);
    free(ps->BatteryHealth);
    free(ps->HardwareSerialNumber);

    free(ps->PowerSourceID);
    free(ps->CurrentCapacity);
    free(ps->MaxCapacity);
    free(ps->DesignCapacity);
    free(ps->NominalCapacity);
    free(ps->TimeToEmpty);
    free(ps->TimeToFullCharge);
    free(ps->Voltage);
    free(ps->Current);
    free(ps->Temperature);

    free(ps->IsCharged);
    free(ps->IsCharging);
    free(ps->InternalFailure);
    free(ps->IsPresent);

    free(ps);
}
*/
import "C"

import (
	"fmt"
	"strconv"

	"github.com/prometheus/client_golang/prometheus"
)

func (c *powerSupplyClassCollector) Update(ch chan<- prometheus.Metric) error {
	psList, err := getPowerSourceList()
	if err != nil {
		return fmt.Errorf("couldn't get IOPPowerSourcesList: %w", err)
	}

	for _, info := range psList {
		labels := getPowerSourceDescriptorLabels(info)
		powerSupplyName := labels["power_supply"]

		if c.ignoredPattern.MatchString(powerSupplyName) {
			continue
		}

		for name, value := range getPowerSourceDescriptorMap(info) {
			if value == nil {
				continue
			}

			ch <- prometheus.MustNewConstMetric(
				prometheus.NewDesc(
					prometheus.BuildFQName(namespace, c.subsystem, name),
					fmt.Sprintf("IOKit Power Source information field %s for <power_supply>.", name),
					[]string{"power_supply"}, nil,
				),
				prometheus.GaugeValue, *value, powerSupplyName,
			)
		}

		pushEnumMetric(
			ch,
			getPowerSourceDescriptorState(info),
			"power_source_state",
			c.subsystem,
			powerSupplyName,
		)

		pushEnumMetric(
			ch,
			getPowerSourceDescriptorBatteryHealth(info),
			"battery_health",
			c.subsystem,
			powerSupplyName,
		)

		var (
			keys   []string
			values []string
		)
		for name, value := range labels {
			if value != "" {
				keys = append(keys, name)
				values = append(values, value)
			}
		}
		fieldDesc := prometheus.NewDesc(
			prometheus.BuildFQName(namespace, c.subsystem, "info"),
			"IOKit Power Source information for <power_supply>.",
			keys,
			nil,
		)
		ch <- prometheus.MustNewConstMetric(fieldDesc, prometheus.GaugeValue, 1.0, values...)

		C.releasePowerSupply(info)
	}

	return nil
}

// getPowerSourceList fetches information from IOKit APIs
//
// Data is provided as opaque CoreFoundation references
// C.getPowerSupplyInfo will convert those objects in something
// easily manageable in Go.
// https://developer.apple.com/documentation/iokit/iopowersources_h
func getPowerSourceList() ([]*C.struct_macos_powersupply, error) {
	infos, err := C.IOPSCopyPowerSourcesInfo()
	if err != nil {
		return nil, err
	}
	defer C.CFRelease(infos)

	psList, err := C.IOPSCopyPowerSourcesList(infos)
	if err != nil {
		return nil, err
	}

	if psList == C.CFArrayRef(0) {
		return nil, nil
	}
	defer C.CFRelease(C.CFTypeRef(psList))

	size, err := C.CFArrayGetCount(psList)
	if err != nil {
		return nil, err
	}

	ret := make([]*C.struct_macos_powersupply, size)
	for i := C.CFIndex(0); i < size; i++ {
		ps, err := C.CFArrayGetValueAtIndex(psList, i)
		if err != nil {
			return nil, err
		}

		dict, err := C.IOPSGetPowerSourceDescription(infos, (C.CFTypeRef)(ps))
		if err != nil {
			return nil, err
		}

		info, err := C.getPowerSupplyInfo(dict)
		if err != nil {
			return nil, err
		}

		ret[int(i)] = info
	}

	return ret, nil
}

func getPowerSourceDescriptorMap(info *C.struct_macos_powersupply) map[string]*float64 {
	return map[string]*float64{
		"current_capacity":      convertValue(info.CurrentCapacity),
		"max_capacity":          convertValue(info.MaxCapacity),
		"design_capacity":       convertValue(info.DesignCapacity),
		"nominal_capacity":      convertValue(info.NominalCapacity),
		"time_to_empty_seconds": minutesToSeconds(info.TimeToEmpty),
		"time_to_full_seconds":  minutesToSeconds(info.TimeToFullCharge),
		"voltage_volt":          scaleValue(info.Voltage, 1e3),
		"current_ampere":        scaleValue(info.Current, 1e3),
		"temp_celsius":          convertValue(info.Temperature),
		"present":               convertValue(info.IsPresent),
		"charging":              convertValue(info.IsCharging),
		"charged":               convertValue(info.IsCharged),
		"internal_failure":      convertValue(info.InternalFailure),
	}
}

func getPowerSourceDescriptorLabels(info *C.struct_macos_powersupply) map[string]string {
	return map[string]string{
		"id":             strconv.FormatInt(int64(*info.PowerSourceID), 10),
		"power_supply":   C.GoString(info.Name),
		"type":           C.GoString(info.Type),
		"transport_type": C.GoString(info.TransportType),
		"serial_number":  C.GoString(info.HardwareSerialNumber),
	}
}

func getPowerSourceDescriptorState(info *C.struct_macos_powersupply) map[string]float64 {
	stateMap := map[string]float64{
		"Off Line":      0,
		"AC Power":      0,
		"Battery Power": 0,
	}

	// This field is always present
	// https://developer.apple.com/documentation/iokit/kiopspowersourcestatekey
	stateMap[C.GoString(info.PowerSourceState)] = 1

	return stateMap
}

func getPowerSourceDescriptorBatteryHealth(info *C.struct_macos_powersupply) map[string]float64 {
	// This field is optional
	// https://developer.apple.com/documentation/iokit/kiopsBatteryHealthkey
	if info.BatteryHealth == nil {
		return nil
	}

	stateMap := map[string]float64{
		"Good": 0,
		"Fair": 0,
		"Poor": 0,
	}

	stateMap[C.GoString(info.BatteryHealth)] = 1

	return stateMap
}

func convertValue(value *C.int) *float64 {
	if value == nil {
		return nil
	}

	ret := new(float64)
	*ret = (float64)(*value)
	return ret
}

func scaleValue(value *C.int, scale float64) *float64 {
	ret := convertValue(value)
	if ret == nil {
		return nil
	}

	*ret /= scale

	return ret
}

// minutesToSeconds converts *C.int minutes into *float64 seconds.
//
// Only positive values will be scaled to seconds, because negative ones
// have special meanings. I.e. -1 indicates "Still Calculating the Time"
func minutesToSeconds(minutes *C.int) *float64 {
	ret := convertValue(minutes)
	if ret == nil {
		return nil
	}

	if *ret > 0 {
		*ret *= 60
	}

	return ret
}

func pushEnumMetric(ch chan<- prometheus.Metric, values map[string]float64, name, subsystem, powerSupply string) {
	for state, value := range values {
		ch <- prometheus.MustNewConstMetric(
			prometheus.NewDesc(
				prometheus.BuildFQName(namespace, subsystem, name),
				fmt.Sprintf("IOKit Power Source information field %s for <power_supply>.", name),
				[]string{"power_supply", "state"}, nil,
			),
			prometheus.GaugeValue, value, powerSupply, state,
		)
	}
}
