#!/usr/bin/python3
# vim:expandtab:autoindent:tabstop=4:shiftwidth=4:filetype=python:tw=0

#############################################################################
#
# Copyright (c) 2016 Dell Computer Corporation
# by Srinivas Gowda <srinivas_g_gowda@dell.com>
# Dual Licenced under GNU GPL and OSL
# Contributor: (c) 2019 Marcin Mielniczuk <marmistrz dot dev at zoho dot eu>
#
#############################################################################
"""smbios-battery-ctl"""



# import arranged alphabetically
import gettext
import locale
import os
import sys
from enum import Enum
from typing import Tuple

# the following vars are all substituted on install
# this bin isnt byte-compiled, so this is ok
pythondir="/usr/lib/python3.13/site-packages"
clidir="/usr/share/smbios-utils"
# end vars

# import all local modules after this.
sys.path.insert(0,pythondir)
sys.path.insert(0,clidir)

import cli
from libsmbios_c import smbios_token, smbios, smi, system_info as sysinfo, localedir, GETTEXT_PACKAGE
from libsmbios_c.trace_decorator import traceLog, getLog

__VERSION__="2.4.3"

locale.setlocale(locale.LC_ALL, '')
gettext.install(GETTEXT_PACKAGE, localedir)

moduleLog = getLog()
verboseLog = getLog(prefix="verbose.")

class RunTimeBatteryErr(Exception): pass


def command_parse():
    parser = cli.OptionParser(usage=__doc__, version=__VERSION__)
    cli.addStdOptions(parser, passwordOpts=True, securityKeyOpt=True)
    parser.add_option(
        "--battery-charge",
        "-c",
        action="store_true",
        default=False,
        help=_("This will get the Current Battery Charging State")
    )
    parser.add_option(
        "--get-charging-cfg",
        action="store_true",
        default=False,
        help=_("Get the current Primary Battery Charge Configuration")
    )

    choices = [str(mode) for mode in ChargingMode]
    parser.add_option(
        "--set-charging-mode",
        metavar='<MODE>',
        choices=choices,
        help=_(f"Set the current Primary Battery Charge Configuration. Valid choices are: {choices}")
    )
    parser.add_option(
        "--set-custom-charge-interval",
        nargs=2,
        metavar='<START> <END>',
        type='int',
        help=_(
            "Set the percentage bounds for custom charge. Both must be integers. "
            "START must lie in the range [50, 95], "
            "END must lie in the range [55, 100], "
            "END must be at least (START + 5)")
    )
    if len(sys.argv) == 1:
        parser.print_help()
        sys.exit()
    return parser.parse_args()


def is_set(num, pos):
    mask = 1 << pos
    return True if (num & mask) else False

# Return a the byte specified by byte_pos from a double word num
def get_byte(num, byte_pos):
    try :
        if byte_pos < 0 or byte_pos > 3:
            raise RunTimeBatteryErr( "Internal Error: Byte position out of bound - ", byte_pos )
    except RunTimeBatteryErr as e:
        print("\t%s" % e)

    return ((num >> (byte_pos*8)) & 0xFF)


def print_express_charge_status(n):
    if n == 0: print('\t Battery is not present')
    elif n == 1: print('\t Standard')
    elif n == 2: print('\t Express')
    elif n == 3: print('\t One-time express')
    elif n == 0xFF: print('\t Battery is present, and it does not support express charge')
    else : print('\t Unknown')


class ChargingMode(Enum):
    PRIMARILY_AC = 0x0341
    ADAPTIVE = 0x0342
    CUSTOM = 0x0343  # 0x0348 is deprecated, cf. token_list.csv
    STANDARD = 0x0346
    EXPRESS = 0x0347

    def __str__(self) -> str:
        return self.name.lower()

    @staticmethod
    def from_string(s: str) -> 'ChargingMode':
        try:
            return ChargingMode[s.upper()]
        except KeyError:
            raise ValueError()


CUSTOM_CHARGE_START = 0x0349
CUSTOM_CHARGE_END = 0x034A


def get_charging_mode() -> ChargingMode:
    table = smbios_token.TokenTable()
    active_mode = None
    for mode in ChargingMode:
        active = table[mode.value].isActive()
        if active:
            if active_mode is not None:
                raise RunTimeBatteryErr(
                    f"Multiple charging modes enabled: {active_mode} and "
                    f"{mode} enabled at the same time"
                )
            active_mode = mode

    if active_mode is None:
        raise RunTimeBatteryErr("No charging mode enabled")
    return active_mode


def set_charging_mode(mode: ChargingMode) -> None:
    table = smbios_token.TokenTable()
    table[mode.value].activate()


def get_custom_charge_interval() -> Tuple[int, int]:
    table = smbios_token.TokenTable()
    low = table[CUSTOM_CHARGE_START].getString()
    high = table[CUSTOM_CHARGE_END].getString()

    # The intervals are stored as 2-byte little endian integers
    low_num = int.from_bytes(low, "little")
    high_num = int.from_bytes(high, "little")

    return (low_num, high_num)


def set_custom_charge_interval(start: int, end: int) -> None:
    table = smbios_token.TokenTable()
    # Primary Battery Custom Charge Start must be in the range [50, 95] with a step value of 1
    #       and at least 5% less than Primary Custom Charge End
    # Primary Battery Custom Charge End must be in the range [55,100] with a step value of 1
    #       and at least 5% greater than Primary Custom Charge Start
    if end - start < 5:
        raise ValueError("END must be at least (START + 5)")
    if start < 50 or start > 95:
        raise ValueError("START must lie in the range [50, 95]")
    if end < 55 or end > 100:
        raise ValueError("END must lie in the range [55, 100]")
    start_bytes = start.to_bytes(2, "little")
    end_bytes = end.to_bytes(2, "little")
    table[CUSTOM_CHARGE_START].setString(start_bytes)
    table[CUSTOM_CHARGE_END].setString(end_bytes)


def print_primary_battery_cfg() -> None:
    mode = get_charging_mode()
    print(f"Charging mode: {mode}")
    if mode == ChargingMode.CUSTOM:
        interval = get_custom_charge_interval()
        print(f"Charging interval: {interval}")


def PrintBatteryChargingState():
    try:
        res = smi.simple_ci_smi( 4, 12, 0 )
        verboseLog.info( _(" Get Charging State : res[smi.cbRES1]=0x%X,res[smi.cbRES2]=0x%X, res[smi.cbRES3]=0x%X") %
                              (res[smi.cbRES1],res[smi.cbRES2],res[smi.cbRES3]) )

        if res[smi.cbRES1] != 0:
            raise RunTimeBatteryErr( _(" Info: Unable to read Current Battery Charging State on this system\n") )

        # cbRES2 byte 0 gives supported Features
        print(_(" \n Supported battery charging features: "))
        if is_set (res[smi.cbRES2], 0): print("\t Express Charging")
        if is_set (res[smi.cbRES2], 1): print("\t Charge Disable")
        if (get_byte(res[smi.cbRES2], 0) == 0 ): print("\t NIL")

        print(_(" \n Battery charging Status: "))
        # cbRES2 byte 1 gives the status of supported Features
        if is_set ((get_byte(res[smi.cbRES2], 1)), 0): print("\t One or more batteries are present that support Express charge")
        if is_set ((get_byte(res[smi.cbRES2], 1)), 1): print("\t Charging is disabled")
        if (get_byte(res[smi.cbRES2], 1) == 0 ): print("\t NIL")

        # cbRes3, bytes 0-3 contain the status of batteries 0-3 respectively
        for i in range(4):
            expr_status = get_byte(res[smi.cbRES3], i)
            if(expr_status):
                print(_("\n Battery %d Express Charge State:") % (i))
                print_express_charge_status(expr_status)

    except RunTimeBatteryErr as e:
        print("\t%s" % e)


def main():
    exit_code = 0
    (options, args) = command_parse()
    cli.setup_std_options(options)

    try:
        if options.battery_charge:
            print(_("Libsmbios version : %s") % sysinfo.get_library_version_string())
            print(_("smbios-battery-ctl version : %s") % __VERSION__)
            verboseLog.info( _(" You can use smbios-battery-ctl utility to view/modify battery settings"))
            PrintBatteryChargingState()
        if options.get_charging_cfg:
            print_primary_battery_cfg()
        if options.set_charging_mode is not None:
            mode = ChargingMode.from_string(options.set_charging_mode)
            set_charging_mode(mode)
            print(f"Charging mode has been set to: {mode}")
        if options.set_custom_charge_interval is not None:
            (low, high) = options.set_custom_charge_interval
            set_custom_charge_interval(low, high)
            print(f"Custom charge interval has been set to ({low}, {high})")

    except (smi.SMIExecutionError, ) as e:
        exit_code=3
        moduleLog.info( _("ERROR: Could not execute SMI.") )
        verboseLog.info( _("The smi library returned this error:") )
        verboseLog.info( str(e) )
        moduleLog.info( cli.standardFailMessage )

    except (smbios.TableParseError, smbios_token.TokenTableParseError) as e:
        exit_code=3
        moduleLog.info( _("ERROR: Could not parse system SMBIOS table.") )
        verboseLog.info( _("The smbios library returned this error:") )
        verboseLog.info( str(e) )
        moduleLog.info( cli.standardFailMessage )

    except (smbios_token.TokenManipulationFailure,) as e:
        exit_code=4
        moduleLog.info( _("ERROR: Could not manipulate system token.") )
        verboseLog.info( _("The token library returned this error:") )
        verboseLog.info( str(e) )
        moduleLog.info( cli.standardFailMessage )

    return exit_code

if __name__ == "__main__":
    sys.exit( main() )


# cbClass 4
# cbSelect 12
# Battery Charging
#
# cbArg1 determines the function to be performed
# On return:
# cbRES1         Standard return codes (0, -1, -2)
#
# cbArg1 0x0 = Get Current State
# cbRes2, byte 0	Supported feature bits
#	bit 0	Express charge is supported (by the BIOS)
#	bit 1	Charge disable is supported
#	bits 2-7	Reserved for future use
# cbRes2, byte 1	Status bits
#	bit 0	One or more batteries are present that support Express charge
#	bit 1	Charging is disabled
#	bits 2-7	Reserved for future use
# If the system supports Express Charge, cbRes3, bytes 0-3 contain the status of batteries 0-3 respectively (numbered as with the Class 8 function).
# cbRes3, byte n	Battery n express charge status:
#	0	Battery is not present
#	1	Standard
#	2	Express
#	3	One-time express
#	0xFF	Battery is present, and it does not support express charge
#
# cbArg2, byte 0	Set charge disable state:
#	0	Off (enable charging)
#	1	On (disable charging)
#
# cbArg2, bytes 0-3 contain the new state of batteries 0-3 respectively
# 	Battery Status:
#	00h	High
#	01h	Low
#	02h 	Critical
#	03h	Charging
#
# cbArg2, byte n Battery n state:
#	0	No change (state remains as it was previously)
#	1	Standard . Express charge not supported by the battery
#	2	Express
#	3	One-time express
#
#
# cbClass 8
# cbSelect 16
# Set Battery Desktop Mode
#
# cbArg1 determines the function to be performed
# On return:
# cbRES1         Standard return codes (0, -1, -2)
#
# cbArg1, byte 0
#	Mode Enablement
#	0		Disable desktop mode
#	1		Fully enable desktop mode
#	2 - 255	Undefined
#
# On return:
# cbRES1         Standard return codes (0, -1, -2)
#
#
# cbClass 8
# cbSelect 17
# Get Battery Desktop Mode
#
# On return:
# cbRES1         Standard return codes (0, -1, -2)
#
# cbRes2	Status
#		Bit 0		1 = Supported, 0 = Not Supported
#		Bit 1		1 = Desktop Mode, 0 = Laptop Mode
#		Bit 2		1 = Battery is ready to enter desktop mode, 0 = Not ready
#		Bit 3 to 31	Unused
