"""
This module contains some example Generative Adversarial Networks for testing.

The classes StupidToyPointGan and StupidToyListGan are not really Networks. This classes are used
for testing the interface. Hope your actually GAN will perform better than this two.

The class SimpleGan is a simple standard Generative Adversarial Network.
"""


import numpy as np
import math

from library.interfaces import GanBaseClass

def square(x):
    return (x*x)

def fold0(f, xs):
    if xs == []:
        return None
   
    s = xs[0]
    for x in xs[1:]:
        s = f(s, x)
    return s

def fold1(f, s0, xs):
    if xs == []:
        return None
   
    s = s0
    for x in xs:
        s = f(s, x)
    return s

def dist(x, y):
    return math.sqrt(fold1(lambda s, a: s + square(a[0] - a[1]), 0, list(zip(x, y))))

def minDistPointToSet(x, setB):
    return fold0(lambda m,y: min(m,y), [dist(x,y) for y in setB])

def minDistSetToSet(setA, setB):
    return fold0(lambda m,x: min(m, minDistPointToSet(x,setB)), setA)

def normInf(xs):
    return fold0(lambda m, x: max(m, abs(x)), xs)

def norm2Sq(xs):
    return fold0(lambda s, x: s + (x*x), xs)

def norm2(xs):
    return math.sqrt(norm2Sq(xs))


def minmax(xs):
    if xs == []:
        return None
   
    (mi, mx) = (xs[0][1], xs[0][1])
    for x in xs[1:]:
        mi = min(mi, x[1])
        mx = max(mx, x[1])
    return (mi, mx)


def createSquare(pointCount, noiseSize):
    noise = [
        [np.random.uniform(-1.0, 1.0) for n in range(noiseSize)]
        for m in range(pointCount)
        ]
    return np.array(noise)

def createDisc(pointCount, noiseSize):
    noise = []
    for n in range(pointCount):
        p = [np.random.uniform(-1.0, 1.0)]
        for m in range(noiseSize - 1):
            d = norm2Sq(p)
            d = math.sqrt(1.0 - d)
            p.append(np.random.uniform(0.0 - d, d))
        noise.append(p)
    return np.array(noise)

class SpheredNoise(GanBaseClass):
    """
    A class for a simple GAN.
    """
    def __init__(self, noiseSize=101):
        self.isTrained = False
        self.noiseSize = noiseSize
        self.disc = []
        self.reset()

    def reset(self, _dataSet):
        """
        Resets the trained GAN to an random state.
        """
        self.pointDists = []
        self.nextId = 0
        self.numPoints = 0
        self.nextDiscPoint = 0
        self.minDist = 0.0

    def train(self, dataset):
        majoritySet = dataset.data0
        minoritySet = dataset.data1
        trainDataSize = minoritySet.shape[0]
        numOfFeatures = minoritySet.shape[1]
        
        print(f"Train {majoritySet.shape[0]}/{trainDataSize} points")

        if minoritySet.shape[0] <= 0 or majoritySet.shape[0] <= 0:
            raise AttributeError("Train: Expected each data class to contain at least one point.")

        if numOfFeatures <= 0:
            raise AttributeError("Train: Expected at least one feature.")

        print("-> new disc")
        self.disc = createDisc(self.noiseSize, minoritySet.shape[1])
        print("-> calc distances")
        self.pointDists = list(filter(lambda x: x[1] > 0.0, [(x, minDistPointToSet(x, majoritySet)) for x in minoritySet]))
        print("-> statistics")
        self.nextId = 0
        self.numPoints = len(self.pointDists)
        self.isTrained = True
        (minD, maxD) = minmax(self.pointDists)
        self.minDist = minD
        print(f"trained {trainDataSize} points min:{minD} max:{maxD}")

    def generateDataPoint(self):
        (x, d) = self.pointDists[self.nextId]
        self.nextId = (self.nextId + 1) % self.numPoints
        disc = (0.5 * self.minDist) * self.disc
        p = disc[self.nextDiscPoint]
        self.nextDiscPoint = (self.nextDiscPoint + 1) % disc.shape[0]
        return p


    def generateData(self, numOfSamples=1):
        return np.array([self.generateDataPoint() for n in range(numOfSamples)])
