#!/usr/bin/python
# coding=utf-8
import multiprocessing
import sys

from storage import HopfieldStorageNet
from settler import HopfieldSettler

from utils import random_binary_array


class HopfieldFactorFinder(object):

    def __init__(self, num_nodes, num_samples, start=1, stop=20, step=1):
        self.num_nodes = num_nodes
        self.num_samples = num_samples

        # In percentages of num_nodes
        self.start = start
        self.stop = stop
        self.step = step

    def run(self):
        print "Running with {} nodes, trying from {}% to {}% memories and {} samples" \
              .format(self.num_nodes, self.start, self.stop, self.num_samples)

        results = self.run_all()

        self.print_results(results)

    def run_all(self):
        specs = [(self.num_nodes, self.num_samples,num_memories)
                 for num_memories in self.all_num_memories()]
        pool = multiprocessing.Pool()
        try:
            results = pool.map_async(run_for, specs)\
                          .get(9999999999)  # use a long timeout for Ctrl-C
        except KeyboardInterrupt:
            print "\nKilled."
            sys.exit()
        results = dict(zip(self.all_num_memories(), results))
        return results

    def all_num_memories(self):
        last = -1
        all_nums = []
        for percentage in xrange(self.start, self.stop + 1, self.step):
            num = max(1, int((float(percentage)/100.0) * self.num_nodes))
            if num > last:
                all_nums.append(num)
                last = num

        return all_nums

    def print_results(self, results):
        for num, percent_retrievable in results.iteritems():
            print "With {} memories ({:0.1f}%) - got {:0.1f}% retrieval" \
                  .format(
                      num,
                      self.get_percentage(num),
                      percent_retrievable
                  )

    def get_percentage(self, num):
        return (100.0 * num) / self.num_nodes


def run_for(args):
    num_nodes, num_samples, num_memories = args
    try:
        inspector = HopfieldFactorInspector(num_nodes,
                                            num_memories,
                                            num_samples=num_samples)
        return inspector.inspect()
    except KeyboardInterrupt:
        pass


class HopfieldFactorInspector(object):
    def __init__(self, num_nodes, num_memories, num_samples=5):
        self.num_nodes = num_nodes
        self.num_memories = num_memories
        # How many samples to draw as multiplier of num_memories
        self.num_samples = num_samples

    def inspect(self):
        num_successes = 0.0
        for i in xrange(self.num_samples):
            result = self.inspect_once()
            if result:
                num_successes += 1
        return (num_successes * 100.0) / self.num_samples

    def inspect_once(self):
        memories = self.generate_random_memories()
        net = HopfieldStorageNet(memories)

        num_hits = 0

        for memory in memories:
            net.set_nodes(memory)
            HopfieldSettler(net).settle()
            if (net.get_nodes() == memory).all():
                num_hits += 1

        return num_hits == self.num_memories

    def generate_random_memories(self):
        return [
            random_binary_array(self.num_nodes)
            for i in xrange(self.num_memories)
        ]

    def get_memory_index(self, memories, nodes):
        for i, memory in enumerate(memories):
            if (nodes == memory).all():
                return i
        return None


if __name__ == '__main__':
    from argparse import ArgumentParser
    parser = ArgumentParser(
        description="Tries to demonstrate the Hopfield limitation factor (that you can store max 0.15N memories in an N-node network) experimentally."
    )
    parser.add_argument('-n',
                        '--num-nodes',
                        dest='num_nodes',
                        help='How many nodes to use in the hopfield network',
                        type=int)
    parser.add_argument('-s',
                        '--num-samples',
                        dest='num_samples',
                        help='How many times to sample each percentage',
                        type=int)
    parser.add_argument('-f',
                        '--from',
                        dest='start',
                        help='What percentage to start at',
                        type=int)
    parser.add_argument('-t',
                        '--to',
                        dest='stop',
                        help='What percentage to stop at',
                        type=int)
    parser.set_defaults(num_nodes=20, num_samples=10, start=1, stop=20)
    args = parser.parse_args()
    HopfieldFactorFinder(**args.__dict__).run()
