# This file contains our mathematical function.

import math

print("Defining my functions")


def myfunc(x):
    """
    Calculates cos(pi*x) - exp(cos(x)).
    """
    return math.cos(math.pi * x) - math.exp(math.cos(x))


# Default and keyword arguments
def power_sum(a, b=2, *, c, d=4):
    """
    a       positional (required)
    b=2     positional or keyword (has default)
    *       everything after * must be keyword-only
    c       keyword-only (required)
    d=4     keyword-only (optional, has default)
    """
    return (a ** b) + (c ** d)


# Variable-length *args example
def geometric_mean(*args):
    """
    Calculates the geometric mean of any number of positive values.
    """
    if not args:
        raise ValueError("At least one value is required.")
    product = 1
    for x in args:
        product *= x
    return product ** (1 / len(args))


# Variable-length **kwargs example
def polynomial(x, **coeffs):
    """
    Evaluates a polynomial of x given keyword coefficients.
    Example:
        polynomial(2, a0=1, a1=3, a2=5)
        -> 1 + 3*x + 5*x**2
    """
    result = 0
    for key, value in coeffs.items():
        if key.startswith("a"):
            # Extract the power from the key name, e.g. 'a2' -> 2
            try:
                power = int(key[1:])
            except ValueError:
                raise ValueError(f"Invalid coefficient key: {key}")
            result += value * x**power
    return result


# Combining everything: args, kwargs, defaults
def combine_functions(x, *args, scale=1, shift=0, power=1, **kwargs):
    """
    Applies multiple functions (passed in *args) to x, combines results
    with optional scaling, shifting, and exponentiation.

    Keyword arguments:
        scale : scales the total sum
        shift : added to the final result
        power : raises each function's output to this power
        Any additional keyword arguments are passed into the functions.

    Example:
        combine_functions(pi/4, math.sin, math.cos, scale=2, shift=1)
        -> 1 + 2 * (sin(pi/4)**1 + cos(pi/4)**1)
    """
    total = 0
    for func in args:
        if callable(func):
            value = func(x, **kwargs)
        else:
            value = func
        total += value**power
    return shift + scale * total


# Example function that accepts keyword args
def gaussian(x, mean=0, sigma=1):
    """
    Gaussian.
    Uses keyword-only parameters mean and sigma.
    """
    return math.exp(-((x - mean)**2) / (2 * sigma**2))


# Script test section
if __name__ == "__main__":
    print("Testing myfuncs.py:")

    # 1. Basic positional
    print(f"myfunc(0.5) = {myfunc(0.5):.4f}")

    # 2. Mixed positional + keyword-only
    print(f"power_sum(2, c=3) = {power_sum(2, c=3):.4f}")
    print(f"power_sum(2, 3, c=4) = {power_sum(2, 3, c=4):.4f}")
    print(f"power_sum(a=2, b=3, c=4, d=5) = {power_sum(a=2, b=3, c=4, d=5):.4f}")
    # print(f"power_sum(2, 3, 4) = {power_sum(2, 3, 4)}")

    # 3. *args example
    print(f"geometric_mean(1, 4, 9) = {geometric_mean(1, 4, 9):.4f}")

    # 4. **kwargs example
    print(f"polynomial(2, a0=1, a1=3, a2=5, a3=4) = {polynomial(2, a0=1, a1=3, a2=5, a3=4):.4f}")

    # 5. Combined example
    print(
        f"combine_functions(pi/4, math.sin, math.cos, scale=2, shift=1) = "
        f"{combine_functions(math.pi/4, math.sin, math.cos, scale=2, shift=1):.4f}"
    )

    # 6. Passing kwargs through
    print(f"gaussian(0, mean=1, sigma=0.5) = {gaussian(0, mean=1, sigma=0.5):.4f}")
    print(f"combine_functions(0, gaussian, scale=2, mean=1, sigma=0.5) = "
          f"{combine_functions(0, gaussian, scale=2, mean=1, sigma=0.5):.4f}")
    print(f"combine_functions(1, gaussian, gaussian, scale=2, power=2, mean=1, sigma=0.5) = "
          f"{combine_functions(1, gaussian, gaussian, scale=2, power=2, mean=1, sigma=0.5):.4f}")

    print("\nAll tests completed.")
