from tabnanny import verbose
import numpy as np
import math
import random

def tweak(point, delta):
    # delta is the max change in either direction
    # for this problem the max change is the same for x and y
    new_x = point[0] + delta * random.uniform(-1, 1)
    new_y = point[1] + delta * random.uniform(-1, 1)
    return (new_x, new_y)

def function_value(x, y):
    return (
        math.sin(3 * math.pi * x) ** 2
        + (x - 1) ** 2 * (1 + math.sin(3 * math.pi * y) ** 2)
        + (y - 1) ** 2 * (1 + math.sin(2 * math.pi * y) ** 2)
    )

def random_point():
    return (random.uniform(-10, 10), random.uniform(-10, 10))



def hill_climbing(delta, verbose=False):
    
    cur_sol = random_point()

    # the asterisk below is called "unpacking" and its a short way of saying
    # cur_score = function_value(cur_sol[0], cur_sol[1])
    cur_score = function_value(*cur_sol)

    failures = 0
    # we will stop when 10000 *consecutive* tweaks do not find a better solution
    while failures < 100_000:
        new_sol = tweak(cur_sol, delta)
        new_score = function_value(*new_sol)

        failures += 1

        if new_score < cur_score:
            cur_sol = new_sol
            cur_score = new_score
            if verbose:
                print(f"New best solution with score: {cur_score} (after {failures} failures)")
            failures = 0

    return cur_sol

def n_trials_steepest_ascent_hill_climbing(n, delta, verbose=False):
    
    cur_sol = random_point()
    iter = 0

    while True:
        iter += 1

        n_tweaks = [tweak(cur_sol, delta) for _ in range(n)]
        best_tweak = min(n_tweaks, key=lambda x: function_value(*x))

        if function_value(*best_tweak) < function_value(*cur_sol):
            cur_sol = best_tweak

        if verbose:
            print(f"Iter {iter}: score = {function_value(*cur_sol)}")


def hill_climbing_with_random_restarts(delta):
    best_sol = None
    best_score = None

    iter = 0
    while True:
        iter += 1
        print(f"HHwRR iteration {iter}")
        sol = hill_climbing(delta)
        score = function_value(*sol)

        if best_sol is None or score < best_score:
            best_sol = sol
            best_score = score

        print(f"\tResult of this iteration: {score}")
        print(f"\tBest solution found so far: {best_score}")

# hill_climbing(0.05, True)
# n_trials_steepest_ascent_hill_climbing(100000, 0.05, True)
# hill_climbing_with_random_restarts(0.05)