'''
Created on 7 Dec 2019

@author: Keith.Gough


Advent of Code 2019 - Day 7: Amplification Circuit

Find the combination of phase settings for the amplifiers that gives the maximum gain.
Amplifier output is generated by instruction code computer program

Phase setting is input to int_code_computer plus the input value of the signal being amplified.
Can only use each phase setting once across each amplifier
Phase settings are in the range 0-4
Input to Amp A is 0.

    O-------O  O-------O  O-------O  O-------O  O-------O
0 ->| Amp A |->| Amp B |->| Amp C |->| Amp D |->| Amp E |-> (to thrusters)
    O-------O  O-------O  O-------O  O-------O  O-------O

What is the combination of phase settings that gives max output


Part 2: Amps are now setup in a feedback loop
Phase settings are in the range 5-9
Input to Amp A is 0.

What is the combination of phase settings that gives max output

      O-------O  O-------O  O-------O  O-------O  O-------O
0 -+->| Amp A |->| Amp B |->| Amp C |->| Amp D |->| Amp E |-.
   |  O-------O  O-------O  O-------O  O-------O  O-------O |
   |                                                        |
   '--------------------------------------------------------+
                                                            |
                                                            v
                                                     (to thrusters)


'''

import itertools
import threading
import logging

import int_code_computer as icc

LOGGER = logging.getLogger(__name__)

PHASE_VALUES_1 = [0, 1, 2, 3, 4]
PHASE_VALUES_2 = [5, 6, 7, 8, 9]

# Permutations can be generated using Heap's Algorithm (see wikipedia) but luckily Python
# does this for us...

PHASE_VALUE_PERMUTATIONS_1 = list(itertools.permutations(PHASE_VALUES_1))
PHASE_VALUE_PERMUTATIONS_2 = list(itertools.permutations(PHASE_VALUES_2))

def run_permutation(program, phase_values):
    """ Calculate the output from the amplifier bank for the
        given set of phase settings
    """
    next_input_data = 0

    for i in range(0, 5):
        computer = icc.IntCodeComputer(program, [phase_values[i], next_input_data])
        computer.run_program()
        next_input_data = computer.result[-1]

    return next_input_data
def find_max_output():
    """ Find the maximum amplifier output  for all the given phase settings

        Max Output = 262086, Max Phase Settings = (2, 1, 4, 0, 3)
    """
    program = icc.load_data("day_07_data.txt")

    max_output = 0
    max_phase = None
    for phase_settings in PHASE_VALUE_PERMUTATIONS_1:
        output = run_permutation(program, phase_settings)
        if output > max_output:
            max_output = output
            max_phase = phase_settings
        #print(f"Phase settings={phase_settings}, Output={output}")

    print('Part 1:')
    print(f"Max Output = {max_output}, Max Phase Settings = {max_phase}")

def run_feedback_loop(phase_settings):
    """ Modify the computer to use Queues for inputs and outputs.
        Computer will block if input required and input Queue is empty.

        Will run each computer in a separate thread so they can run
        simultaneously.
    """
    program = icc.load_data("day_7_data.txt")

    thread_pool = []
    thread_pool_size = 5

    # Setup pool of amp int code computers with feedback configuration
    amp_pool = []
    amp_pool.append(icc.IntCodeComputer(program))
    amp_pool.append(icc.IntCodeComputer(program, amp_pool[0].output))
    amp_pool.append(icc.IntCodeComputer(program, amp_pool[1].output))
    amp_pool.append(icc.IntCodeComputer(program, amp_pool[2].output))
    amp_pool.append(icc.IntCodeComputer(program, amp_pool[3].output, amp_pool[0].input))


    # Pre-load all the pahse settings
    for i in range(thread_pool_size):
        amp_pool[i].input.put(phase_settings[i])

    # Give Amp 0 the starting input value of 0
    amp_pool[0].input.put(0)

    for i in range(thread_pool_size):
        # Setup a worker thread for each amp and insert the phase settings
        # input each of the input queues.
        worker_thread = threading.Thread(target=amp_pool[i].run_program)

        # Deamonise the threads (so the die nicely when the script ends
        # May not be needed but stops stray threads being left behind if
        # we have a crash or some other weird exit.
        worker_thread.daemon = True

        # Start each of the threads running
        worker_thread.start()

        # Add them to pool list so we can keep track of them
        thread_pool.append(worker_thread)

    # Wait here for all the threads to exit properly
    for thd in thread_pool:
        thd.join()

    return amp_pool[4].result
def find_max_feedback_output():
    """ Find max amplifier output with feedback """
    max_output = 0
    max_phase_settings = None
    for phase_settings in PHASE_VALUE_PERMUTATIONS_2:
        result = run_feedback_loop(phase_settings)
        if result[-1] > max_output:
            max_output = result[-1]
            max_phase_settings = phase_settings

    print('\nPart 2:')
    print(f'Max Feedback Output = {max_output}, Max Feedback Phase Settings = {max_phase_settings}')

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    find_max_output()
    find_max_feedback_output()
