"""
Contains every classes related to the graphics engine.
"""

from pytgf.logic.physics import Renderable, TileManager, World, LogicLoop, array_format

from time import sleep

import numpy
import moderngl
import traceback
import sys

DEFAULT_SHADER_SPRITE_VERTEX = """#version 330

uniform mat4 position;
uniform mat4 projection;

in vec2 vertices;
in vec2 textures;

out vec2 textureCoordinates;

void main() {
    textureCoordinates = textures;

    gl_Position = projection * position * vec4(vertices, 0.0, 1.0);
}
"""

DEFAULT_SHADER_SPRITE_FRAGMENT = """#version 330

uniform sampler2D sampler;

in vec2 textureCoordinates;
out vec4 outColor;

void main() {
    outColor = texture(sampler, textureCoordinates);
}
"""

DEFAULT_SHADER_WORLD_VERTEX = """#version 330

uniform mat4 projection;
uniform mat4 position;

in vec2 vertices;
in vec2 textures;

out vec2 textureCoordinates;

void main() {
    textureCoordinates = textures;

    gl_Position = projection * position * vec4(vertices, 0.0, 1.0);
}
"""

DEFAULT_SHADER_WORLD_FRAGMENT = """#version 330

uniform sampler2D levelTexture;
uniform sampler2D tilesTexture;

uniform int levelWidth;
uniform int levelHeight;

uniform int tilesWidth;
uniform int tilesHeight;

in vec2 textureCoordinates;
out vec4 outColor;

void main() {
    vec2 coordinates = vec2(textureCoordinates.xy);

    if(textureCoordinates.y * levelHeight == floor(coordinates.y * levelHeight)) {
        coordinates = vec2(coordinates.x, coordinates.y + 1.0 / (levelHeight * levelHeight));
    }

    if(textureCoordinates.x * levelHeight == floor(coordinates.x * levelHeight)) {
        coordinates = vec2(coordinates.x + 1.0 / (levelWidth * levelWidth), coordinates.y);
    }

    vec2 tilePos = vec2(
        floor(coordinates.x * levelWidth),
        floor(coordinates.y * levelHeight)
    );

    vec4 tileSpec = texture(levelTexture, coordinates.yx);

    float render = round(tileSpec.x * 255);

    if(render != 0) {
        float index = round(255 * (tileSpec.y + 256 * tileSpec.z));

        vec2 texIndex = vec2(
            floor(mod(index, tilesWidth)) / tilesWidth,
            floor(index / tilesWidth) / tilesHeight
        );

        vec2 texOffset = vec2(
            (coordinates.x * levelWidth - tilePos.x) / tilesWidth,
            (coordinates.y * levelHeight - tilePos.y) / tilesHeight
        );

        outColor = texture(tilesTexture, texIndex + texOffset);
    } else {
        outColor = vec4(0.0, 0.0, 0.0, 0.0);
    }
}
"""

class ProjectionMatrix:
    """
    A standardized representation of the projection matrix used for the rendering.

    The projection matrix allows to transforms in different ways the rendering. It has to be use for the camera
    projection and the different renderers.

    Attributes
    ----------
    matrix: numpy.ndarray
        The projection matrix in the numpy array format.

    Methods
    -------
    orthographic(right, left, bottom, top)
        Applies a 2D orthographic projection to the matrix.
    scale(scale)
        Scales the projection matrix.
    translate(translation)
        Translates the projection matrix.
    rotate(angle)
        Rotates the projection matrix.
    dot(matrix)
        Composes the projection matrix with another matrix.
    inverse_transform(vector)
        Applies an inverse transformation to a vector.
    """

    def __init__(self, matrix: numpy.ndarray = None):
        """
        Initializes the ProjectionMatrix.

        Parameters
        ----------
        matrix: numpy.ndarray, optional
            The projection matrix in the numpy array format (the identity by default).
        """

        self.matrix = matrix if matrix is not None else numpy.identity(4, dtype=numpy.float32)

    def orthographic(self, right: float, left: float, bottom: float, top: float) -> "ProjectionMatrix":
        """
        Applies a 2D orthographic projection to the matrix.

        This kind of transformation should only be used by the camera by describing its field of view.

        Parameters
        ----------
        right: float
            The extreme right position of the projection.
        left: float
            The extreme left position of the projection.
        bottom: float
            The extreme bottom position of the projection.
        top: float
            The extreme top position of the projection

        Returns
        -------
        projection: ProjectionMatrix
            The current projection matrix with the transformation applied.
        """

        orthographic = numpy.array([
            [2.0 / (right - left), 0.0, 0.0, - (right + left) / (right - left)],
            [0.0, 2.0 / (top - bottom), 0.0, - (top + bottom) / (top - bottom)],
            [0.0, 0.0, - 1.0, 0.0],
            [0.0, 0.0, 0.0, 1.0]
        ], dtype=numpy.float32)

        self.matrix = self.matrix.dot(orthographic)

        return self

    def scale(self, scale: [tuple, numpy.ndarray]) -> "ProjectionMatrix":
        """
        Scales the projection matrix.

        Parameters
        ----------
        scale: [tuple, numpy.ndarray]
            The scale factor vector along both directions.

        Returns
        -------
        projection: ProjectionMatrix
            The current projection matrix with the transformation applied.
        """

        scale = array_format(scale, dtype=numpy.float32)

        scale_matrix = numpy.array([
            [scale[0], 0.0, 0.0, 0.0],
            [0.0, scale[1], 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0],
            [0.0, 0.0, 0.0, 1.0]
        ], dtype=numpy.float32)

        self.matrix = self.matrix.dot(scale_matrix)

        return self

    def translate(self, translation: [tuple, numpy.ndarray]) -> "ProjectionMatrix":
        """
        Translates the projection matrix.

        Parameters
        ----------
        translation: [tuple, numpy.ndarray]
            The translation vector along both directions.

        Returns
        -------
        projection: ProjectionMatrix
            The current projection matrix with the transformation applied.
        """

        translation = array_format(translation, dtype=numpy.float32)

        translation_matrix = numpy.array([
            [1.0, 0.0, 0.0, 0.0],
            [0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0],
            [translation[0], translation[1], 0.0, 1.0]
        ], dtype=numpy.float32)

        self.matrix = self.matrix.dot(translation_matrix)

        return self

    def rotate(self, angle: float) -> "ProjectionMatrix":
        """
        Rotates the projection matrix.

        Parameters
        ----------
        angle: float
            The angle by which the matrix is rotated.

        Returns
        -------
        projection: ProjectionMatrix
            The current projection matrix with the transformation applied.
        """

        rotation_matrix = numpy.array([
            [numpy.cos(angle), - numpy.sin(angle), 0.0, 0.0],
            [numpy.sin(angle), numpy.cos(angle), 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0],
            [0.0, 0.0, 0.0, 1.0]
        ], dtype=numpy.float32)

        self.matrix = self.matrix.dot(rotation_matrix)

        return self

    def dot(self, matrix: numpy.ndarray) -> "ProjectionMatrix":
        """
        Composes the projection matrix with another matrix.

        Parameters
        ----------
        matrix: numpy.ndarray
            The matrix with which the projection will be composed.

        Returns
        -------
        projection: ProjectionMatrix
            The current projection matrix with the transformation applied.
        """

        self.matrix = self.matrix.dot(matrix).astype(numpy.float32)

        return self

    def inverse_transform(self, vector: [tuple, numpy.ndarray]) -> numpy.ndarray:
        """
        Applies an inverse transformation to a vector.

        Parameters
        ----------
        vector: [tuple, numpy.ndarray]
            The vector to transform in the local coordinate system.

        Returns
        -------
        vector: numpy.ndarray
            The transformed vector.
        """

        vector = array_format(vector, dtype=numpy.float32)

        full_vector = numpy.array([[vector[0]], [vector[1]], [1], [0]], dtype=numpy.float32)

        inverse_matrix = numpy.array([
            [1 / self.matrix[0, 0], 0.0, 0.0, 0.0],
            [0.0, 1 / self.matrix[1, 1], 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0],
            [- self.matrix[3, 0], - self.matrix[3, 1], 0.0, 1.0]
        ], dtype=numpy.float32)

        transformed = inverse_matrix.dot(full_vector)

        return numpy.array((transformed[0, 0], transformed[1, 0]), dtype=numpy.float32)

    def __str__(self) -> str:
        """
        Returns a description string of the object.

        Returns
        -------
        string: str
            The string object description.
        """

        return "ProjectionMatrix[matrix=" + str(self.matrix) + "]"

class Camera:
    """
    The main rendering camera object.

    The world is rendered through the camera. The field of view specifies the number of "pixels" that can be rendered on
    each axis.

    Attributes
    ----------
    size: numpy.ndarray
        The size of the field of view.
    position: numpy.ndarray
        The position vector of the center point.
    viewport: numpy.ndarray
        The viewport of the camera.
    projection_matrix: ProjectionMatrix
        The projection matrix of the 2D orthographic projection.

    Methods
    -------
    transform_world(position)
        Transforms the position vector into the camera coordinate system.
    """

    def __init__(self, size: [tuple, numpy.ndarray]):
        """
        Initializes the Camera.

        Parameters
        ----------
        size: [tuple, numpy.ndarray]
            The size of the field of view.
        """

        self._size = array_format(size, dtype=numpy.float32)
        self._position = array_format((0, 0), dtype=numpy.float32)
        self._last_position = array_format((0, 0), dtype=numpy.float32)

        self.viewport = self.size / 2

        self.projection_matrix = ProjectionMatrix(). \
            orthographic(- self.viewport[0], self.viewport[0], - self.viewport[1], self.viewport[1])

    @property
    def position(self) -> numpy.ndarray:
        """
        The position property representing the position of the center point of the camera.
        """

        if not numpy.array_equal(self._position, self._last_position):
            pass

        return self._position

    @position.setter
    def position(self, position: [tuple, numpy.ndarray]) -> None:
        """
        Setter function for the position vector.

        Parameters
        ----------
        position: [tuple, numpy.ndarray]
            The position of the center point of the camera.
        """

        self._position = array_format(position, dtype=numpy.float32)

    @property
    def size(self) -> numpy.ndarray:
        """
        The size property representing the width and the height of the field of view.
        """

        return self._size

    @size.setter
    def size(self, size: [tuple, numpy.ndarray]) -> None:
        """
        Setter function for the size vector.

        Parameters
        ----------
        bounds: [tuple, numpy.ndarray]
            The size vector of the field of view.
        """

        self._size = array_format(size, dtype=numpy.float32)
        self.viewport = self._size / 2

        self.projection_matrix = ProjectionMatrix(). \
            orthographic(- self.viewport[0], self.viewport[0], - self.viewport[1], self.viewport[1])

    def __str__(self) -> str:
        """
        Returns a description string of the object.

        Returns
        -------
        string: str
            The string object description.
        """

        return "Camera[position=" + str(self._position) + ", size=" + str(self.size) + "]"

class Texture:
    """
    A texture OpenGl object.

    This class allows to buffer a numpy array into the OpenGL memory. Note that the image arrays have to be in the RGBA
    unsigned byte format, hence the depth must be 4. When this object is deleted, the OpenGL memory is automatically
    cleared.

    Methods
    -------
    bind(texture)
        Binds the texture to an OpenGL sampler.
    """

    SAMPLER_SPRITE = 0
    SAMPLER_LEVEL = 1
    SAMPLER_TILES = 2

    def __init__(self, context: moderngl.Context, texture: numpy.ndarray, scale: bool = False):
        """
        Initializes the Texture.

        Parameters
        ----------
        context: moderngl.Context
            The main OpenGL context.
        texture: numpy.ndarray
            The image array in a RGBA unsigned byte format.
        scale: bool, optional
            Treats the image as a gray scale if set to True (only used by the level renderer).
        """

        self._width = texture.shape[0]
        self._height = texture.shape[1]

        if scale:
            texture = numpy.flip(texture, axis=1)

            transform = numpy.zeros((self._width, self._height, 3), dtype=numpy.int32)
            transform[:, :, 0] = (texture != 0).astype(numpy.int32) * 255
            transform[:, :, 1] = (texture - 1) % 256
            transform[:, :, 2] = (texture - 1) // 256 % 256

            buffer = transform.reshape(self._width * self._height * 3).astype(numpy.ubyte)

            self._texture = context.texture((self._height, self._width), 3, buffer.tobytes())

        else:
            buffer = texture.reshape(self._width * self._height * 4).astype(numpy.ubyte)

            self._texture = context.texture((self._height, self._width), 4, buffer.tobytes())

        self._texture.filter = (moderngl.NEAREST, moderngl.NEAREST)
        self._texture.repeat_x = False
        self._texture.repeat_y = False

    def __del__(self) -> None:
        """
        Cleans up the GPU memory by releasing the texture buffer.
        """

        self._texture.release()

    def bind(self, sampler: int) -> None:
        """
        Binds the texture to an OpenGL sampler.

        Parameters
        ----------
        sampler: int
            The index of the simple to which the texture will be bound.

        Raises
        ------
        ValueError
            If the sampler index is lesser than 0 or greater than 31.
        """

        if 0 <= sampler < 32:
            self._texture.use(sampler)
        else:
            raise ValueError("The sampler used must be located between 0 and 31.")

    def __str__(self) -> str:
        """
        Returns a description string of the object.

        Returns
        -------
        string: str
            The string object description.
        """

        return "Texture[width=" + str(self._width) + ", height=" + str(self._height) + "]"

class Model:
    """
    A model OpenGL object.

    This object holds the different OpenGL VBOs and VAO used to render a model on screen. When this object is deleted,
    the OpenGL memory is automatically cleared.

    Methods
    -------
    render()
        Renders the model.
    """

    ATTRIBUTE_VERTICES = "vertices"
    ATTRIBUTE_TEXTURES = "textures"

    def __init__(self, context: moderngl.Context, program: moderngl.Program, vertices_buffer: numpy.ndarray,
                 texture_coordinates_buffer: numpy.ndarray, indices_buffer: numpy.ndarray):
        """
        Initializes the Model.

        Parameters
        ----------
        context: moderngl.Context
            The main OpenGL context.
        program: moderngl.Program
            The GLSL shader program used for the rendering.
        vertices_buffer: numpy.ndarray
            The vertex coordinates array.
        texture_coordinates_buffer: numpy.ndarray
            The texture coordinates array.
        indices_buffer: numpy.ndarray
            The texture indices array.
        """

        self._vbo_vertices = context.buffer(vertices_buffer.astype(numpy.float32).tobytes())
        self._vbo_texture_coordinates = context.buffer(texture_coordinates_buffer.astype(numpy.float32).tobytes())
        self._vbo_indices = context.buffer(indices_buffer.astype(numpy.int32).tobytes())

        vao_content = [
            (self._vbo_vertices, '2f', Model.ATTRIBUTE_VERTICES),
            (self._vbo_texture_coordinates, '2f', Model.ATTRIBUTE_TEXTURES)
        ]

        self._vao = context.vertex_array(program, vao_content, self._vbo_indices)

    def __del__(self) -> None:
        """
        Cleans up the GPU memory by releasing the VAO and VBOs.
        """

        self._vao.release()

        self._vbo_vertices.release()
        self._vbo_texture_coordinates.release()
        self._vbo_indices.release()

    def render(self) -> None:
        """
        Renders the model.
        """

        self._vao.render()

    def __str__(self) -> str:
        """
        Returns a description string of the object.

        Returns
        -------
        string: str
            The string object description.
        """

        return "Model[]"

class ShaderProgram:
    """
    A program GLSL object.

    The object represents a GLSL shader program. It is used to render the models. When this object is deleted, the
    OpenGL memory is automatically cleared.

    Attributes
    ----------
    program: moderngl.Program
        The GLSL program object.

    Methods
    -------
    set_uniform(name, value)
        Sets the value of a program uniform.
    """

    UNIFORM_POSITION = "position"
    UNIFORM_PROJECTION = "projection"

    UNIFORM_SAMPLER = "sampler"

    UNIFORM_TILES_SAMPLER = "tilesTexture"
    UNIFORM_TILES_WIDTH = "tilesWidth"
    UNIFORM_TILES_HEIGHT = "tilesHeight"

    UNIFORM_LEVEL_SAMPLER = "levelTexture"
    UNIFORM_LEVEL_WIDTH = "levelWidth"
    UNIFORM_LEVEL_HEIGHT = "levelHeight"

    def __init__(self, context: moderngl.Context, source_vertex: str, source_fragment: str):
        """
        Initializes the ShaderProgram.

        Parameters
        ----------
        context: moderngl.Context
            The main OpenGL context.
        source_vertex: str
            The GLSL source code of the vertex shader.
        source_fragment: str
            The GLSL source code of the fragment shader.
        """

        self.program = context.program(vertex_shader=source_vertex, fragment_shader=source_fragment)

    def __del__(self) -> None:
        """
        Cleans up the GPU memory by releasing the program.
        """

        self.program.release()

    def set_uniform(self, name: str, value: any) -> None:
        """
        Sets the value of a program uniform.

        Parameters
        ----------
        name: str
            The name of the program uniform.
        value: any
            The new value of the uniform. Numpy arrays are correctly converted.
        """

        if name in self.program:
            if isinstance(value, numpy.ndarray):
                self.program[name].value = tuple(value.reshape(-1))
            else:
                self.program[name].value = value

    def __str__(self) -> str:
        """
        Returns a description string of the object.

        Returns
        -------
        string: str
            The string object description.
        """

        return "ShaderProgram[]"

class SpriteSet:
    """
    A collection of textures and animation.

    This object contains the textures and the animations used for a sprite.

    Methods
    -------
    register_sprite_texture(texture)
        Registers a new texture.
    register_sprite_animation(period, animation)
        Registers a new animation.
    get_texture(id_animation, pointer)
        Returns the texture to render.
    """

    def __init__(self):
        """
        Initializes the SpriteSet.
        """

        self._textures = []
        self._animations = []

    def register_sprite_texture(self, texture: Texture) -> None:
        """
        Registers a new texture.

        Registers a new texture and gives it the first index available within the sprite set.

        Parameters
        ----------
        texture: Texture
            The texture used for the rendering.
        """

        self._textures.append(texture)

    def register_sprite_animation(self, period: int, animation: tuple) -> None:
        """
        Registers a new animation.

        Registers a new animation and gives it the first index available within the sprite set.

        Parameters
        ----------
        period: int
            The period of the animation expressed in frames (if the animation is static, this should be set to 0).
        animation: tuple of ints
            The indexes of the textures of the animation.
        """

        self._animations.append((period, animation))

    def get_texture(self, id_animation: int, pointer: int) -> (int, Texture):
        """
        Returns the texture to render.

        Returns the texture to render and the next animation pointer. The pointer loop through the whole animation and
        is reset to 0 when the animation is completed.

        Parameters
        ----------
        id_animation: int
            The index of the animation used.
        pointer: int
            The current animation pointer.

        Returns
        -------
        pointer, texture: int, Texture
            The new pointer and the texture to render.
        """

        if self._animations[id_animation][0] == 0 or \
                pointer >= self._animations[id_animation][0] * len(self._animations[id_animation][1]):
            return 0, self._textures[self._animations[id_animation][1][0]]

        texture = self._textures[self._animations[id_animation][1][pointer // self._animations[id_animation][0]]]

        if pointer == self._animations[id_animation][0] * len(self._animations[id_animation][1]) - 1:
            return 0, texture

        return pointer + 1, texture

    def __str__(self) -> str:
        """
        Returns a description string of the object.

        Returns
        -------
        string: str
            The string object description.
        """

        return "SpriteSet[]"

class TileSet:
    """
    A collection of tile texture concatenated.

    For better performance, the tiles texture are only sampled once. Every tile texture are placed on a single tile set
    texture. Note that no more than 65536 tiles texture can be used.

    Attributes
    ----------
    width: int
        The width of the grid (number of texture per row).
    height: int
        The height of the grid (number of texture per column).

    Methods
    -------
    bind(program)
        Binds the tile set texture.
    """

    def __init__(self, texture: Texture, width: int, height: int):
        """
        Initializes the TileSet.

        Parameters
        ----------
        texture: Texture
            The texture containing the tiles organised in grid.
        width: int
            The width of the grid (number of texture per row).
        height: int
            The height of the grid (number of texture per column).

        Raises
        ------
        ValueError
            If the number of texture exceed 65536 or if the size is invalid.
        """

        self._texture = texture

        if width * height > 65536:
            raise ValueError("The maximum tile index is 65535.")

        if width <= 0 or height <= 0:
            raise ValueError("An tiles must have a width/height of at least 1.")

        self.width = width
        self.height = height

    def bind(self, shader: ShaderProgram) -> None:
        """
        Binds the tile set texture.

        Binds the tile set texture to the tiles sampler and updates the program uniforms related to the size.

        Parameters
        ----------
        shader: ShaderProgram
            The shader program used for the world rendering.
        """

        self._texture.bind(Texture.SAMPLER_TILES)

        shader.set_uniform(ShaderProgram.UNIFORM_TILES_WIDTH, self.width)
        shader.set_uniform(ShaderProgram.UNIFORM_TILES_HEIGHT, self.height)

    def __str__(self) -> str:
        """
        Returns a description string of the object.

        Returns
        -------
        string: str
            The string object description.
        """

        return "TileSet[width=" + str(self.width) + ", height=" + str(self.height) + ", " + \
               "texture=" + str(self._texture) + "]"

class ResourceManager(TileManager):
    """
    The graphical resource manager.

    A resource manager must be created to registers the textures used and to render the world. This is only needed if
    the game needs to be rendered. This object should be fully initialized before creating a world. If there is no
    window used for the rendering, this should be created in standalone mode. It contains the main OpenGL context as
    well as the different graphical graphics used for the rendering.

    Attributes
    ----------
    context: moderngl.Context
        The main OpenGL context.
    tile_size: int
        The size of the tiles expressed in distance units.
    scale: float
        The rendering scale factor.
    shader_world: ShaderProgram
        The shader program used for the rendering of the world.
    shader_sprite: ShaderProgram
        The shader program used for the rendering of sprites.
    model_world: Model
        The model used to render the world.
    model_sprite: Model
        The model used to render sprites.
    tile_set: TileSet
        The tile set used for the world rendering.
    sprite_sets: dict of SpriteSet
        The sprite sets used for the sprite rendering.

    Methods
    -------
    attach_frame_buffer(width, height)
        Attaches a new frame buffer in which the rendering will be performed.
    register_collision_map(collision_map)
        Registers a new collision map.
    register_tile(id_collision_map, id_texture)
        Registers a new tile.
    register_tile_set(texture, width, height)
        Registers the main tile set.
    register_background(name)
        Registers a new background.
    register_background_texture(texture)
        Registers a new background texture.
    register_background_layer(name, id_texture, ratio_x, ratio_y)
        Registers a new background layer.
    register_sprite_set(sprite_set)
        Registers a new sprite set.
    register_sprite_texture(sprite_set, texture)
        Registers a new sprite texture.
    register_sprite_animation(sprite_set, period, animation)
        Registers a new sprite animation.
    get_collision_map(id_tile)
        Returns the collision map associated to the tile.
    get_id_texture(id_tile)
        Returns the texture associated to the tile.
    get_last_frame_buffer(index, viewport)
        Returns the latest rendered frame.
    render_model(model)
        Renders a model.
    render_background(name, camera)
        Renders the background.
    render_sprite(renderable, camera)
        Renders a sprite.
    increment_animation_pointer(renderable)
        Increments the animation pointer of the renderable.
    """

    BUFFER_QUAD_POSITION = numpy.array([-0.5, 0.5, 0.5, 0.5, 0.5, -0.5, -0.5, -0.5], dtype=numpy.float64)
    BUFFER_QUAD_TEXTURE_POSITION = numpy.array([0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0], dtype=numpy.float64)
    BUFFER_QUAD_INDICES = numpy.array([0, 1, 2, 2, 3, 0], dtype=numpy.uint)

    def __init__(self, tile_size: int, scale: float = 1.0, standalone: bool = False, width: int = 800,
                 height: int = 600, glsl_version: int = 330, shader_world: tuple = None, shader_sprite: tuple = None):
        """
        Initializes the ResourceManager.

        Parameters
        ----------
        tile_size: int
            The size of the tiles expressed in distance units.
        scale: float, optional
            The rendering scale factor.
        standalone: bool, optional
            This have to be set to True if there is no window used. It will create a virtual window to render the game.
        width: int, optional
            Used in standalone mode, the width of the virtual window.
        height: int, optional
            Used is standalone mode, the height of the virtual window.
        glsl_version: int, optional
            The required GLSL version.
        shader_world: tuple of strings, optional
            The couple of source code for the fragment and vertex shaders used for the rendering of the world.
        shader_sprite: tuple of strings, optional
            The couple of source code for the fragment and vertex shaders used for the rendering of sprites.
        """

        TileManager.__init__(self, tile_size)

        if not standalone:
            self.context = moderngl.create_context(require=glsl_version)
            self._frame_buffers = [self.context.screen]
        else:
            self.context = moderngl.create_standalone_context(require=glsl_version)
            self._frame_buffers = [self.context.framebuffer([self.context.renderbuffer((width, height))])]

        self._frame_buffers[0].use()

        self.context.clear(1.0, 1.0, 1.0, 1.0)
        self.context.enable(moderngl.BLEND)
        self.context.blend_func = (moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA)

        self.scale = scale

        if not shader_world:
            self.shader_world = ShaderProgram(
                self.context, DEFAULT_SHADER_WORLD_VERTEX, DEFAULT_SHADER_WORLD_FRAGMENT
            )
        else:
            self.shader_world = ShaderProgram(self.context, shader_world[0], shader_world[1])

        self.shader_world.set_uniform(ShaderProgram.UNIFORM_LEVEL_SAMPLER, Texture.SAMPLER_LEVEL)
        self.shader_world.set_uniform(ShaderProgram.UNIFORM_TILES_SAMPLER, Texture.SAMPLER_TILES)

        if not shader_sprite:
            self.shader_sprite = ShaderProgram(
                self.context, DEFAULT_SHADER_SPRITE_VERTEX, DEFAULT_SHADER_SPRITE_FRAGMENT
            )
        else:
            self.shader_sprite = ShaderProgram(self.context, shader_sprite[0], shader_sprite[1])

        self.shader_sprite.set_uniform(ShaderProgram.UNIFORM_SAMPLER, Texture.SAMPLER_SPRITE)

        self.model_world = Model(
            self.context, self.shader_world.program, ResourceManager.BUFFER_QUAD_POSITION,
            ResourceManager.BUFFER_QUAD_TEXTURE_POSITION, ResourceManager.BUFFER_QUAD_INDICES
        )

        self.model_sprite = Model(
            self.context, self.shader_sprite.program, ResourceManager.BUFFER_QUAD_POSITION,
            ResourceManager.BUFFER_QUAD_TEXTURE_POSITION, ResourceManager.BUFFER_QUAD_INDICES
        )

        self.tile_set = None
        self.sprite_sets = {}

        self._background_textures = []
        self._backgrounds = {}

        self._reuse = False

    def __del__(self) -> None:
        """
        Release the current OpenGL context.
        """

        self.context.release()

    def register_tile_set(self, texture: numpy.ndarray, width: int, height: int) -> None:
        """
        Registers the main tile set.

        Parameters
        ----------
        texture: numpy.ndarray
            The texture array containing the tiles organised in grid.
        width: int
            The width of the grid (number of texture per row).
        height: int
            The height of the grid (number of texture per column).
        """

        self.tile_set = TileSet(Texture(self.context, texture), width, height)

        self.tile_set.bind(self.shader_world)

    def register_background(self, name: str) -> None:
        """
        Registers a new background.

        Registers a new background under the specified name. A background consists of a bunch of background layers
        stacked on each others.

        Parameters
        ----------
        name: str
            The name of the background.
        """

        self._backgrounds[name] = []

    def register_background_texture(self, texture: numpy.ndarray) -> None:
        """
        Registers a new background texture.

        Registers a new background texture and gives it the first available index.

        Parameters
        ----------
        texture: numpy.ndarray
            The texture array of the background.
        """

        self._background_textures.append(Texture(self.context, texture))

    def register_background_layer(self, name: str, id_texture: int, ratio_x: float, ratio_y: float) -> None:
        """
        Registers a new background layer.

        Registers a new background layer with the specified camera ratios. The ratios indicates how fast the background
        layer moves relatively to the camera speed. A ratio of 0 mean that the background is static and a ratio of 1
        means that the background moves at the same speed as the camera.

        Parameters
        ----------
        name: str
            The name of the background.
        id_texture: int
            The index of the background texture.
        ratio_x: float
            The background over camera speed ratio along the x axis.
        ratio_y: float
            The background over camera speed ratio along the y axis.
        """

        self._backgrounds[name].append((id_texture, ratio_x, ratio_y))

    def register_sprite_set(self, sprite_set: str) -> None:
        """
        Registers a new sprite set.

        Registers a new sprite set under the specified name.

        Parameters
        ----------
        sprite_set: str
            The name of the sprite set.
        """

        self.sprite_sets[sprite_set] = SpriteSet()

    def register_sprite_texture(self, sprite_set: str, texture: numpy.ndarray) -> None:
        """
        Registers a new sprite texture.

        Registers a new sprite texture and gives it the first index available within the sprite set.

        Parameters
        ----------
        sprite_set: str
            The name of the sprite set.
        texture: numpy.ndarray
            The texture array of the sprite.
        """

        self.sprite_sets[sprite_set].register_sprite_texture(Texture(self.context, texture))

    def register_sprite_animation(self, sprite_set: str, period: int, animation: tuple) -> None:
        """
        Registers a new sprite animation.

        Registers a new sprite animation and gives it the first index available within the sprite set.

        Parameters
        ----------
        sprite_set: str
            The name of the sprite set.
        period: int
            The period of the animation expressed in frames (if the animation is static, this should be set to 0).
        animation: tuple of ints
            The indexes of the textures of the animation.
        """

        self.sprite_sets[sprite_set].register_sprite_animation(period, animation)

    def attach_frame_buffer(self, width: int, height: int) -> None:
        """
        Attaches a new frame buffer in which the rendering will be performed.

        Parameters
        ----------
        width: int
            The width of the frame.
        height: int
            The height of the frame.
        """

        self._frame_buffers.append(self.context.framebuffer([self.context.renderbuffer((width, height))]))

        self._reuse = True

    def get_frame_buffer(self, index: int, viewport=None) -> numpy.ndarray:
        """
        Returns the latest rendered frame.

        Parameters
        ----------
        index: int
            The index of the frame buffer (0 corresponds to the screen).
        viewport: tuple of int
            The part of the screen to buffer (takes the full frame if set to None).

        Returns
        -------
        frame: numpy.ndarray
            The latest rendered frame.
        """

        frame_buffer = self._frame_buffers[index]

        if viewport is None:
            viewport = (0, 0) + frame_buffer.size

        return numpy.flip(numpy.frombuffer(
            frame_buffer.read(viewport=viewport), dtype=numpy.uint8
        ).reshape((viewport[3], viewport[2], 3)), axis=0)

    def render_model(self, model: Model) -> None:
        """
        Renders a model.

        Renders a model, either world or sprite. This function should be called after all the necessary uniforms of
        the associated shader program are set. The model will be rendered to every attached frame buffer.

        Parameters
        ----------
        model: Model
            The model to render.
        """

        if not self._reuse:
            model.render()

            return

        for frame_buffer in self._frame_buffers:
            frame_buffer.use()
            model.render()

    def render_background(self, name: str, camera: Camera) -> None:
        """
        Renders the background.

        Renders the specified background all over the screen, hence this function should be called directly after the
        frame buffer has been cleared and before the rendering of the level and the entities.

        Parameters
        ----------
        name: str
            The name of the background rendered.
        camera: Camera
            The camera used for the rendering.
        """

        for layer in self._backgrounds[name]:
            self._background_textures[layer[0]].bind(0)

            position_delta = ((- camera.position * self.scale * layer[1:]) % camera.size) / camera.size

            for i in range(-1 + (position_delta[0] == 0), 1):
                for j in range(- 1 + (position_delta[1] == 0), 1):
                    position = ProjectionMatrix().translate(position_delta + (i, j)).scale((2, 2))

                    self.shader_sprite.set_uniform(ShaderProgram.UNIFORM_POSITION, position.matrix)
                    self.render_model(self.model_sprite)

    def render_sprite(self, renderable: Renderable, camera: Camera) -> None:
        """
        Renders a sprite.

        Renders a renderable sprite considering its animation played and its position in the world.

        Parameters
        ----------
        renderable: Renderable
            The renderable to render.
        camera: Camera
            The camera used for the rendering.
        """

        if renderable.is_transiting():
            animation_pointer, texture = self.sprite_sets[renderable.sprite_set].get_texture(
                renderable.id_animation_transition, renderable.animation_pointer
            )
        else:
            animation_pointer, texture = self.sprite_sets[renderable.sprite_set].get_texture(
                renderable.id_animation, renderable.animation_pointer
            )

        texture.bind(0)

        flip_array = numpy.array((renderable.flip_horizontally, renderable.flip_vertically), dtype=numpy.float32)

        position = ProjectionMatrix().scale(
            renderable.texture_bounds.bounds * (1 - flip_array * 2)
        ).rotate(
            renderable.angle
        ).translate(
            (renderable.position + renderable.texture_bounds.bounds / 2 + renderable.texture_bounds.position)
        ).scale(
            (-1, 1)
        ).translate(
            (camera.position[0], - camera.position[1])
        ).scale(
            (self.scale, self.scale)
        )

        self.shader_sprite.set_uniform(ShaderProgram.UNIFORM_POSITION, position.matrix)
        self.render_model(self.model_sprite)

        renderable.animation_pointer = animation_pointer

    def increment_animation_pointer(self, renderable: Renderable) -> None:
        """
        Increments the animation pointer of the renderable.

        Increments the animation pointer of the specified renderable. Note that when a renderable is displayed, its
        animation pointer is automatically displayed, this function should be used to keep the animation of non rendered
        object (such as out of camera view objects) synchronized with the others.

        Parameters
        ----------
        renderable: Renderable
            The renderable of which the animation pointer will be incremented.
        """

        if renderable.is_transiting():
            animation_pointer, _ = self.sprite_sets[renderable.sprite_set].get_texture(
                renderable.id_animation_transition, renderable.animation_pointer
            )
        else:
            animation_pointer, _ = self.sprite_sets[renderable.sprite_set].get_texture(
                renderable.id_animation, renderable.animation_pointer
            )

        renderable.animation_pointer = animation_pointer

    def __str__(self) -> str:
        """
        Returns a description string of the object.

        Returns
        -------
        string: str
            The string object description.
        """

        return "ResourceManager[tile_size=" + str(self.tile_size) + ", scale=" + str(self.scale) + ", " + \
               "tile_set=" + str(self.tile_set) + "]"

class LevelRenderer:
    """
    The renderer used for level rendering.

    This class stores a texture of the tiles of the level. This allow to render the level faster than rendering the
    tiles individually. Each time the level is modified, the texture of the level should be updated.

    Methods
    -------
    render(camera)
        Renders the level.
    update_tiles(tiles)
        Updates the tile level array.
    """

    def __init__(self, resources: ResourceManager, tiles: numpy.ndarray, scale: int):
        """
        Initializes the LevelRenderer.

        Parameters
        ----------
        resources: ResourceManager
            The main resource manager used for the rendering.
        tiles: numpy.ndarray
            The tiles array of the level.
        scale: int
            The size of the tiles expressed in distance units.
        """

        self._resources = resources
        self._tiles = tiles.copy()
        self._scale = scale

        self._width = tiles.shape[0]
        self._height = tiles.shape[1]

        self._texture = Texture(resources.context, self._tiles, scale=True)

        self._bind()

    def _bind(self) -> None:
        """
        Binds the level into the GPU memory.
        """

        self._texture.bind(Texture.SAMPLER_LEVEL)

        self._resources.shader_world.set_uniform(ShaderProgram.UNIFORM_LEVEL_WIDTH, self._width)
        self._resources.shader_world.set_uniform(ShaderProgram.UNIFORM_LEVEL_HEIGHT, self._height)

    def render(self, camera: Camera) -> None:
        """
        Renders the level.

        Parameters
        ----------
        camera: Camera
            The camera used for the rendering.
        """

        position = ProjectionMatrix().translate(
            (0.5, 0.5)
        ).scale(
            (- self._scale * self._width, self._scale * self._height)
        ).translate(
            (camera.position[0], - camera.position[1])
        ).scale(
            (self._resources.scale, self._resources.scale)
        )

        self._resources.shader_world.set_uniform(ShaderProgram.UNIFORM_POSITION, position.matrix)
        self._resources.shader_world.set_uniform(ShaderProgram.UNIFORM_PROJECTION, camera.projection_matrix.matrix)

        self._resources.render_model(self._resources.model_world)

    def update_tiles(self, tiles: numpy.ndarray):
        """
        Updates the tile level array.

        Updates the local copy of the level array. This will update the OpenGL object only if change occurred.

        Parameters
        ----------
        tiles: numpy.ndarray
            The tiles array of the level.
        """

        if not numpy.array_equal(self._tiles, tiles):
            self._tiles = tiles.copy()

            self._texture = Texture(self._resources.context, self._tiles, scale=True)

            if self._tiles.shape[0] != self._width or self._tiles.shape[1] != self._height:
                self._width = tiles.shape[0]
                self._height = tiles.shape[1]

                self._bind()
            else:
                self._texture.bind(Texture.SAMPLER_LEVEL)

    def __str__(self) -> str:
        """
        Returns a description string of the object.

        Returns
        -------
        string: str
            The string object description.
        """

        return "LevelRenderer[texture=" + str(self._texture) + "]"

class WorldRenderer:
    """
    The renderer used for world rendering.

    This class allows to render the level and the entities of the world. It relies on a level renderer. The entities out
    of the camera field are not displayed for better performance.

    Methods
    -------
    render(camera)
        Renders the world.
    """

    def __init__(self, resources: ResourceManager, world: World):
        """
        Initializes the WorldRenderer.

        Parameters
        ----------
        resources: ResourceManager
            The main resource manager used for the rendering.
        world: World
            The main world to render.
        """

        self._resources = resources
        self._world = world

        self._level_renderer = LevelRenderer(resources, self._world.tiles, self._resources.tile_size)

    def render(self, camera: Camera) -> None:
        """
        Renders the world.

        Renders the world by rendering the following in order: background, level and entities. Note that the entities
        out of the screen are not rendered for better performances.

        Parameters
        ----------
        camera: Camera
            The camera used for the rendering.
        """

        self._resources.shader_sprite.set_uniform(ShaderProgram.UNIFORM_PROJECTION, ProjectionMatrix().matrix)

        if self._world.background is not None:
            self._resources.render_background(self._world.background, camera)

        self._level_renderer.update_tiles(self._world.tiles)
        self._level_renderer.render(camera)

        self._resources.shader_sprite.set_uniform(ShaderProgram.UNIFORM_PROJECTION, camera.projection_matrix.matrix)

        for world_object in self._world.world_objects:
            if isinstance(world_object, Renderable):
                if not world_object.should_be_destroyed and world_object.visible:
                    if numpy.logical_and(*(
                            (numpy.abs(world_object.position - camera.position) -
                             world_object.texture_bounds.bounds) * self._resources.scale < camera.viewport)):
                        self._resources.render_sprite(world_object, camera)
                    else:
                        self._resources.increment_animation_pointer(world_object)

    def __str__(self) -> str:
        """
        Returns a description string of the object.

        Returns
        -------
        string: str
            The string object description.
        """

        return "WorldRenderer[world=" + str(self._world) + ", level_renderer=" + str(self._level_renderer) + "]"

class RenderLoop(LogicLoop):
    """
    The main game loop function with addition of rendering routine.

    This object is callable, hence is should be called as if it was a function. When called, the loop starts and run the
    logic function at each tick, at a specified tick rate and run the render function at each frame, at a specified
    frame rate.

    Methods
    -------
    stop()
        Stops the main loop.
    """

    def __init__(self, tick_per_second: float, frame_per_second: float, function_logic: callable,
                 function_render: callable):
        """
        Initializes the RenderLoop.

        Parameters
        ----------
        tick_per_second: float
            The tick rate of the loop. It correspond to the number of times the logic will be performed per second.
        frame_per_second: float
            The frame rate of the loop. It correspond to the number of times the frame will be rendered per second.
        function_logic: callable
            The logic function of the loop called each tick.
        function_render: callable
            The render function of the loop called each frame.
        """

        LogicLoop.__init__(self, tick_per_second, function_logic)

        self._function_render = function_render
        self._frame_period = 1.0 / frame_per_second

        self._reference_extra_render = 0

        self._frame = 0

    def __call__(self, number_of_ticks: int = None, max_speed: bool = False) -> None:
        """
        Runs the main logic function.

        Runs the main logic function for a given number of tick. At max speed, the logic is done as fast as possible,
        but the rendering is still synchronized with the frame rate. Note that if an exception is raised, the loop will
        stop.

        Parameters
        ----------
        number_of_ticks: int, optional
            The number of ticks over which the logic will be performed. Il this parameter is not set, the loop will run
            until the stop function is called.
        max_speed: bool, optional
            Ignores the tick rate and runs at maximum speed if set to True.
        """

        target_tick = self._tick + number_of_ticks if number_of_ticks else -1

        current_time = LogicLoop._get_current_time()
        reference_logic = current_time - self._reference_extra_logic
        reference_render = current_time - self._reference_extra_render

        self._running = True

        try:
            if max_speed:
                while self._running and self._tick != target_tick:
                    self._do_logic()

                    if LogicLoop._get_current_time() - reference_render >= self._frame_period:
                        self._do_render()

                        reference_render += self._frame_period

            else:
                while self._running and self._tick != target_tick:
                    current_time = LogicLoop._get_current_time()

                    if current_time - reference_logic >= self._tick_period:
                        self._do_logic()

                        while current_time - reference_logic >= self._tick_period:
                            reference_logic += self._tick_period

                    current_time = LogicLoop._get_current_time()

                    if current_time - reference_render >= self._frame_period:
                        self._do_render()

                        while current_time - reference_render >= self._frame_period:
                            reference_render += self._frame_period

                    sleep_time = min(self._tick_period + reference_logic, self._frame_period + reference_render) - \
                        LogicLoop._get_current_time()

                    if sleep_time > 0:
                        sleep(sleep_time)
        except BaseException as error:
            print("An error occurred during the game loop:", file=sys.stderr)
            traceback.print_tb(error.__traceback__)
            print(error.__class__.__name__ + ":", error, file=sys.stderr)
        finally:
            current_time = LogicLoop._get_current_time()

            self._reference_extra_logic = current_time - reference_logic
            self._reference_extra_render = current_time - reference_render

            self._running = False

    def _do_render(self) -> None:
        """
        Calls the render routine.
        """

        self._function_render(self._frame)

        self._frame += 1

    def __str__(self) -> str:
        """
        Returns a description string of the object.

        Returns
        -------
        string: str
            The string object description.
        """

        return "RenderLoop[tick=" + str(self._tick) + ", frame=" + str(self._frame) + ", " + \
               "running=" + str(self._running) + "]"


import os
import chess, chess.uci, chess.pgn
import numpy as np
import threading

class StockfishEval:
    """
    Evaluates board positions from a .pgn file in parallel.
    Saves a npy dictionary of FEN positions and their Stockfish evaluations.
    """
    def __init__(self,
                 stockfish_exe,
                 pgn_file,
                 score_dict_filename,
                 threads,
                 export_inc):
        if not str(score_dict_filename).endswith('.npy'):
            score_dict_filename += '.npy'

        self.stockfish_exe = stockfish_exe
        self.pgn_file = pgn_file
        self.score_dict_filename = score_dict_filename
        self.threads = int(threads)
        self.export_inc = export_inc
        self.score_dict = {}

        self.import_hash_table('threads\\FICS\\known_scores_merged.npy')

    def import_hash_table(self, dict_file):
        # Attempt to load already processed boards
        if os.path.isfile(dict_file):
            self.score_dict = np.load(dict_file).item()
            print('Imported hash table of length {}', str(len(self.score_dict)))
        else:
            print('No hash table found. Creating new hash table.')

    def export_hash_table(self):
        np.save(self.score_dict_filename, self.score_dict)

    def eval_thread(self, thread_num, pgn_file=None):
        engine = chess.uci.popen_engine(self.stockfish_exe)
        if pgn_file is None:
            pgn = open(self.pgn_file)
        else:
            pgn = open(pgn_file)

        def export_thread_hash_table():
            print('Saving progress for thread {} len: {}'.format(thread_num, len(thread_score_dict)))
            filename = str(self.score_dict_filename) + '_' + str(thread_num) + '.npy'
            np.save('threads\\' + filename, thread_score_dict)

        engine.uci()
        engine.setoption({"Threads": 1, "Hash": 64})
        info_handler = chess.uci.InfoHandler()
        engine.info_handlers.append(info_handler)

        game_num = 0
        games_processed_by_thread = 0

        if pgn_file is None:
            while game_num < thread_num:
                chess.pgn.skip_game(pgn)
                game_num += 1

        game = chess.pgn.read_game(pgn)
        thread_score_dict = {}
        while game is not None:
            board = game.board()
            engine.ucinewgame()
            print('Processing game {} on thread {}'.format(game_num, thread_num))

            move_number = 0
            for move in game.mainline_moves():
                board.push(move)

                # Check if board has already been evaluated
                if board.fen() not in self.score_dict and \
                        board.fen() not in thread_score_dict:
                    engine.position(board)

                    # Provide deeper evaluation for earlier moves
                    # if move_number < 5:
                    #     eval_time = 300
                    # elif move_number < 15:
                    #     eval_time = 200
                    # else:
                    #     eval_time = 200

                    try:
                        engine.go(movetime=200, ponder=False)
                    except chess.uci.EngineTerminatedException as err:
                        print('Unexpected engine error:')
                        print(err)

                    engine.stop()
                    # engine.go(depth=25, ponder=False)
                    print(info_handler.info['score'][1][0])
                    print('----')
                    score = info_handler.info['score'][1].cp
                    mate = info_handler.info['score'][1].mate

                    # If Stockfish finds mate, then give an extreme score
                    if mate is not None:
                        if mate > 0:
                            if board.turn:
                                score = 10000
                            else:
                                score = -10000
                        else:
                            if board.turn:
                                score = -10000
                            else:
                                score = 10000
                    elif not board.turn:
                        # Adjust score if Stockfish is playing black's turn
                        score *= -1

                    thread_score_dict[board.fen()] = score

                move_number += 1

            # game = chess.pgn.read_game(self.pgn)
            if pgn_file is None:
                skip_to = self.threads + game_num
                while game_num < skip_to:
                    chess.pgn.skip_game(pgn)
                    game_num += 1
            else:
                game_num += 1

            game = chess.pgn.read_game(pgn)
            games_processed_by_thread += 1
            if games_processed_by_thread % self.export_inc == 0:
                export_thread_hash_table()

    def execute_parallel_eval(self):
        procs = []
        # If pgn_file is a list of pgn's then assign them to threads.
        if type(self.pgn_file) is list:
            for i in range(self.threads):
                thread_pgn = self.pgn_file[i]
                print('Thread ' + str(i) + ' started. PGN: ' + str(thread_pgn))
                p = threading.Thread(target=self.eval_thread, args=(i, thread_pgn))
                procs.append(p)
                p.start()
        else:
            for i in range(self.threads):
                print('Thread ' + str(i) + ' started.')
                # p = Process(target=self.eval_thread, args=(i, ))
                p = threading.Thread(target=self.eval_thread, args=(i,))
                procs.append(p)
                p.start()

        for proc in procs:
            proc.join()

if __name__ == '__main__':
    # KingBase_pgns = ['data\\KingBase\\KingBase2019-A00-A39.pgn',
    #                  'data\\KingBase\\KingBase2019-A40-A79.pgn',
    #                  'data\\KingBase\\KingBase2019-A80-A99.pgn',
    #                  'data\\KingBase\\KingBase2019-B00-B19.pgn',
    #                  'data\\KingBase\\KingBase2019-B20-B49.pgn',
    #                  'data\\KingBase\\KingBase2019-B50-B99.pgn',
    #                  'data\\KingBase\\KingBase2019-C00-C19.pgn',
    #                  'data\\KingBase\\KingBase2019-C20-C59.pgn',
    #                  'data\\KingBase\\KingBase2019-C60-C99.pgn',
    #                  'data\\KingBase\\KingBase2019-D00-D29.pgn',
    #                  'data\\KingBase\\KingBase2019-D30-D69.pgn',
    #                  'data\\KingBase\\KingBase2019-D70-D99.pgn',
    #                  'data\\KingBase\\KingBase2019-E00-E19.pgn',
    #                  'data\\KingBase\\KingBase2019-E20-E59.pgn',
    #                  'data\\KingBase\\KingBase2019-E60-E99.pgn']

    # ficsgamesdb_2018_chess2000_nomovetimes_79725
    # ficsgamesdb_2017_chess2000_nomovetimes_80572
    evaluator = StockfishEval('Stockfish\\stockfish_10_x64.exe',
                              'data\\FICS\\ficsgamesdb_2017_chess2000_nomovetimes_80572.pgn',
                              'TEST',
                              16,
                              75,)

    evaluator.execute_parallel_eval()


from keras.preprocessing import image
import numpy as np
from glob import glob
import pickle
import random
import cv2
from model.extract_bottleneck_features import extract_InceptionV3
from keras.applications.resnet50 import preprocess_input
from PIL import ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
from keras.layers import GlobalAveragePooling2D
from keras.layers import Dense
from keras.models import Sequential
from keras.applications.resnet50 import ResNet50
import urllib.request
import os

random.seed(8675309)
path = __file__.replace('model/dog_app.py', '')

def load_dog_names():
    """
    load dog names sorted by their label

    Returns:
        list: list of dog names
    """
    with open(os.path.join(path, 'model/names.pkl'), 'rb') as f:
        dog_names = pickle.load(f)
        dog_names = [x.split('/')[-1][4:] for x in dog_names]
    return dog_names

def load_dataset(path):
    """
    Given the path to the dataset of images, extract the file names and labels from the file name

    Args:
        path (str): path to the set images

    Returns:
        list: file paths
        list: the name of the dog breed for each image
    """
    files = glob(path + '*')
    targets = [x.split('/')[-1][:-10] for x in files]
    return files, targets

def path_to_tensor(img_path):
    """
    Given an image path laod the image and return a tensor

    Args:
        img_path (str): path to the image
    Returns:
        tf.tensor
    """
    img = image.load_img(img_path, target_size=(224, 224))
    x = image.img_to_array(img)
    return np.expand_dims(x, axis=0)

def get_model(p=os.path.join(path, 'model/DogInceptionV3Data.npz')):
    """
    Function that load feature maps form a pre-trained model. Then it will add a pooling layer and two dense layers to
    tune the top of the CNN model to adapt to dog images. The model is already train on a set of dog images and it's
    saved in `weights.best.Inception.hdf5`.

    Args:
        p (str): path to the DogInceptionV3Data.npz file that contains the transformed features. If file doesn't exist,
        it will be downloaded from AWS.
    Returns:
        Keras Model
    """
    url = 'https://s3-us-west-1.amazonaws.com/udacity-aind/dog-project/DogInceptionV3Data.npz'
    if not os.path.exists(p):
        urllib.request.urlretrieve(url, p)
    bottleneck_features = np.load(p)
    train_Inception = bottleneck_features['train']
    Inception_model = Sequential()
    Inception_model.add(GlobalAveragePooling2D(input_shape=train_Inception.shape[1:]))
    Inception_model.add(Dense(256, activation='relu'))
    Inception_model.add(Dense(133, activation='softmax'))
    Inception_model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'])
    Inception_model.load_weights(os.path.join(path, 'saved_models/weights.best.Inception.hdf5'))
    return Inception_model

class DogDetection:
    """
    The class handles all the necessary function for detection faces, and dogs in an image. After detection faces and
    dogs this class allows you to find the dog breed of the dog in your image.

    Example:
        This example uses :class:`.DogDetection` to instantiate an object that handles all the necessary functions
        to predict the dog breed of a dog image.

        >>> dd = DogDetection()
        >>> dd.which_dog('static/sample_dog_output.png')
        >>> dd.dog_detector('static/sample_dog_output.png')
    """
    def __init__(self):
        url = 'https://s3-us-west-1.amazonaws.com/udacity-aind/dog-project/DogInceptionV3Data.npz'
        p = os.path.join(path, 'inception_v3_weights_tf_dim_ordering_tf_kernels_notop.h5')
        if not os.path.exists(p):
            urllib.request.urlretrieve(url, p)
        self.resnet = ResNet50(weights='imagenet')
        self.model = get_model()
        self.dog_names = load_dog_names()
        self.train_files, self.train_targets = load_dataset(os.path.join(path, 'static/images/'))
        self.face_cascade = cv2.CascadeClassifier(os.path.join(path, 'model/haarcascade_frontalface_alt.xml'))

    def face_detector(self, img_path):
        """
        Using the path to the image find the bounding box around a human face. Then return whether a face is
        detected or not.

        Args:
            img_path (str): Path to the image

        Returns:
            (str): Name of the dog based on the prediction of the Inception model
        """
        img = cv2.imread(img_path)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        faces = self.face_cascade.detectMultiScale(gray)
        return len(faces) > 0

    def Inception_predict_breed(self, img_path):
        """
        Using the path to the image predict the name of the dog

        Args:
            img_path (str): Path to the image

        Returns:
            (str): Name of the dog based on the prediction of the Inception model
        """
        bottleneck_feature = extract_InceptionV3(path_to_tensor(img_path))
        predicted_vector = self.model.predict(bottleneck_feature)
        return self.dog_names[np.argmax(predicted_vector)]

    def ResNet50_predict_labels(self, img_path):
        """
        Using the path to the image it predict the label of the dog breed

        Args:
            img_path (str): Path to the image

        Returns:
            (int): Label of the dog image
        """
        img = preprocess_input(path_to_tensor(img_path))
        return np.argmax(self.resnet.predict(img))

    def dog_detector(self, img_path):
        """
        Function to determine if there is a dog in the image or not

        Args:
            img_path (str): Path to the image

        Returns:
            bool: whether the image is a dog or not
        """
        prediction = self.ResNet50_predict_labels(img_path)
        return (prediction <= 268) & (prediction >= 151)

    def which_dog(self, img_path):
        """
        This function determines the breed of the dog using an input image.
        If the image is not an image of dog, but rather of a human,
        it will find a dog that resembles the picture of the human. If the image is neither human face nor dog face, it will return
        a string indicating "no face detected".

        Args:
            img_path (str): The path to an image
        """
        is_dog = self.dog_detector(img_path)
        is_face = self.face_detector(img_path)
        dog_breed = self.Inception_predict_breed(img_path)
        if is_dog or is_face:

            if is_face:
                message = f"The dog look alike is {dog_breed}"
            else:
                message = dog_breed
        else:
            message = "no face or dog detected"
        return message, dog_breed

    def get_image(self, breed):
        ids = np.where([breed == x for x in self.train_targets])[0]
        idx = np.random.choice(ids)
        image = self.train_files[idx]
        return image

def main():
    import matplotlib.pyplot as plt
    from glob import glob
    import cv2
    files = glob('../images/*')
    breed_detector = DogDetection()
    for f in files:
        message, breed = breed_detector.which_dog(f)
        print(message)
        img = cv2.imread(f)
        cv_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        file = breed_detector.get_image(breed)
        img_similar = cv2.imread(file)
        cv_rgb_similar = cv2.cvtColor(img_similar, cv2.COLOR_BGR2RGB)
        fig, ax = plt.subplots(1, 2, figsize=(8, 4))
        ax[0].imshow(cv_rgb)
        ax[1].imshow(cv_rgb_similar)
        ax[1].set_title(breed)
        plt.pause(0.0001)
    plt.show()

if __name__ == '__main__':
    main()


def selection_11():

    # Library import
    import numpy
    import matplotlib
    import matplotlib.pyplot   as plt
    import matplotlib.gridspec as gridspec

    # Library version
    matplotlib_version = matplotlib.__version__
    numpy_version      = numpy.__version__

    # Histo binning
    xBinning = numpy.linspace(0.0,2000.0,401,endpoint=True)

    # Creating data sequence: middle of each bin
    xData = numpy.array([2.5,7.5,12.5,17.5,22.5,27.5,32.5,37.5,42.5,47.5,52.5,57.5,62.5,67.5,72.5,77.5,82.5,87.5,92.5,97.5,102.5,107.5,112.5,117.5,122.5,127.5,132.5,137.5,142.5,147.5,152.5,157.5,162.5,167.5,172.5,177.5,182.5,187.5,192.5,197.5,202.5,207.5,212.5,217.5,222.5,227.5,232.5,237.5,242.5,247.5,252.5,257.5,262.5,267.5,272.5,277.5,282.5,287.5,292.5,297.5,302.5,307.5,312.5,317.5,322.5,327.5,332.5,337.5,342.5,347.5,352.5,357.5,362.5,367.5,372.5,377.5,382.5,387.5,392.5,397.5,402.5,407.5,412.5,417.5,422.5,427.5,432.5,437.5,442.5,447.5,452.5,457.5,462.5,467.5,472.5,477.5,482.5,487.5,492.5,497.5,502.5,507.5,512.5,517.5,522.5,527.5,532.5,537.5,542.5,547.5,552.5,557.5,562.5,567.5,572.5,577.5,582.5,587.5,592.5,597.5,602.5,607.5,612.5,617.5,622.5,627.5,632.5,637.5,642.5,647.5,652.5,657.5,662.5,667.5,672.5,677.5,682.5,687.5,692.5,697.5,702.5,707.5,712.5,717.5,722.5,727.5,732.5,737.5,742.5,747.5,752.5,757.5,762.5,767.5,772.5,777.5,782.5,787.5,792.5,797.5,802.5,807.5,812.5,817.5,822.5,827.5,832.5,837.5,842.5,847.5,852.5,857.5,862.5,867.5,872.5,877.5,882.5,887.5,892.5,897.5,902.5,907.5,912.5,917.5,922.5,927.5,932.5,937.5,942.5,947.5,952.5,957.5,962.5,967.5,972.5,977.5,982.5,987.5,992.5,997.5,1002.5,1007.5,1012.5,1017.5,1022.5,1027.5,1032.5,1037.5,1042.5,1047.5,1052.5,1057.5,1062.5,1067.5,1072.5,1077.5,1082.5,1087.5,1092.5,1097.5,1102.5,1107.5,1112.5,1117.5,1122.5,1127.5,1132.5,1137.5,1142.5,1147.5,1152.5,1157.5,1162.5,1167.5,1172.5,1177.5,1182.5,1187.5,1192.5,1197.5,1202.5,1207.5,1212.5,1217.5,1222.5,1227.5,1232.5,1237.5,1242.5,1247.5,1252.5,1257.5,1262.5,1267.5,1272.5,1277.5,1282.5,1287.5,1292.5,1297.5,1302.5,1307.5,1312.5,1317.5,1322.5,1327.5,1332.5,1337.5,1342.5,1347.5,1352.5,1357.5,1362.5,1367.5,1372.5,1377.5,1382.5,1387.5,1392.5,1397.5,1402.5,1407.5,1412.5,1417.5,1422.5,1427.5,1432.5,1437.5,1442.5,1447.5,1452.5,1457.5,1462.5,1467.5,1472.5,1477.5,1482.5,1487.5,1492.5,1497.5,1502.5,1507.5,1512.5,1517.5,1522.5,1527.5,1532.5,1537.5,1542.5,1547.5,1552.5,1557.5,1562.5,1567.5,1572.5,1577.5,1582.5,1587.5,1592.5,1597.5,1602.5,1607.5,1612.5,1617.5,1622.5,1627.5,1632.5,1637.5,1642.5,1647.5,1652.5,1657.5,1662.5,1667.5,1672.5,1677.5,1682.5,1687.5,1692.5,1697.5,1702.5,1707.5,1712.5,1717.5,1722.5,1727.5,1732.5,1737.5,1742.5,1747.5,1752.5,1757.5,1762.5,1767.5,1772.5,1777.5,1782.5,1787.5,1792.5,1797.5,1802.5,1807.5,1812.5,1817.5,1822.5,1827.5,1832.5,1837.5,1842.5,1847.5,1852.5,1857.5,1862.5,1867.5,1872.5,1877.5,1882.5,1887.5,1892.5,1897.5,1902.5,1907.5,1912.5,1917.5,1922.5,1927.5,1932.5,1937.5,1942.5,1947.5,1952.5,1957.5,1962.5,1967.5,1972.5,1977.5,1982.5,1987.5,1992.5,1997.5])

    # Creating weights for histo: y12_PT_0
    y12_PT_0_weights = numpy.array([0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0])

    # Creating weights for histo: y12_PT_1
    y12_PT_1_weights = numpy.array([0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0])

    # Creating a new Canvas
    fig   = plt.figure(figsize=(12,6),dpi=80)
    frame = gridspec.GridSpec(1,1,right=0.7)
    pad   = fig.add_subplot(frame[0])

    # Creating a new Stack
    pad.hist(x=xData, bins=xBinning, weights=y12_PT_0_weights+y12_PT_1_weights,\
             label="$signal4TeV$", rwidth=1.0,\
             color="#ce5e60", edgecolor="#ce5e60", linewidth=1, linestyle="solid",\
             bottom=None, cumulative=False, normed=False, align="mid", orientation="vertical")

    pad.hist(x=xData, bins=xBinning, weights=y12_PT_0_weights,\
             label="$signal1TeV$", rwidth=1.0,\
             color="#5954d8", edgecolor="#5954d8", linewidth=1, linestyle="solid",\
             bottom=None, cumulative=False, normed=False, align="mid", orientation="vertical")

    # Axis
    plt.rc('text',usetex=False)
    plt.xlabel(r"p_{T} [ a_{2} ]   ( GeV ) ",\
               fontsize=16,color="black")
    plt.ylabel(r"$\mathrm{Events}$ $(\mathcal{L}_{\mathrm{int}} = 40.0\ \mathrm{fb}^{-1})$ ",\
               fontsize=16,color="black")

    # Boundary of y-axis
    ymax=(y12_PT_0_weights+y12_PT_1_weights).max()*1.1
    ymin=0 # linear scale
    #ymin=min([x for x in (y12_PT_0_weights+y12_PT_1_weights) if x])/100. # log scale
    plt.gca().set_ylim(ymin,ymax)

    # Log/Linear scale for X-axis
    plt.gca().set_xscale("linear")
    #plt.gca().set_xscale("log",nonposx="clip")

    # Log/Linear scale for Y-axis
    plt.gca().set_yscale("linear")
    #plt.gca().set_yscale("log",nonposy="clip")

    # Legend
    plt.legend(bbox_to_anchor=(1.05,1), loc=2, borderaxespad=0.)

    # Saving the image
    plt.savefig('../../HTML/MadAnalysis5job_0/selection_11.png')
    plt.savefig('../../PDF/MadAnalysis5job_0/selection_11.png')
    plt.savefig('../../DVI/MadAnalysis5job_0/selection_11.eps')

# Running!
if __name__ == '__main__':
    selection_11()


import socket
import sqlite3
import sys
import aes_crypt
import rsa_encrypt
import settings
import json
import threading
import numpy as np
from face_recog import *
import base64
import hashlib

def challenge():

    #create host object
    host = Host()

    #Create a hash of the node's public key to send to the auth node for identity verification
    keyhash = str(base64.b64encode(hashlib.sha256(rsa_encrypt.get_pub_key().exportKey("PEM")).digest()),'ascii')

    #creates a payload of the message that identifies that this is a client node that needs to be updated
    payload = "sndU"

    #create a socket to communicate with the auth nodes
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) as s:
        s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 32)

        #Create an empty data and address variable and a socket to recieve the data
        data = ""
        addr = 0
        with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as us:

            #set a timeout for if there is no auth node ready
            us.settimeout(1)
            us.bind(('0.0.0.0', 55551))

            #send the challenge tag to the auth nodes along with a public key to encrypt their return message with
            s.sendto(aes_crypt.aes_enc(rsa_encrypt.get_pub_key_auth(), "who?:" + keyhash), ((host.host, host.port)))

            #Recv a number from the auth node to connect to
            try:
                data, addr = us.recvfrom(4096)
            #if it fails return an error
            except socket.timeout:
                us.close()
                return -1

        #Decrypt the recieved message
        data = aes_crypt.aes_dec(rsa_encrypt.get_priv_key(), data)

        #if message is bad return error
        if data == -1 or data == -2:
            return -1

        #convert data to a string to return to the auth nodes along with the instruction payload
        data = str(data, 'ascii')

        #send payload and return expected address
        s.sendto(aes_crypt.aes_enc(rsa_encrypt.get_pub_key_auth(), "you!:" + data + ":" + payload), ((host.host, host.port)))
        return addr

#Host object to store multicast information
class Host():
    def __init__(self):
        self.host = settings.MULT_ADDR
        self.port = settings.MULT_PORT

#Send selected share to the auth node
def send_share(share, host):

    #Store the share as a string
    payload = "auth:"+ share['id'] + ":" + share['x'] + ":" + share['y']

    #open a socket to the multicast address
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) as s:
        s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 32)

        #encrypt the share with the auth public key and send it to the multicast address
        s.sendto(aes_crypt.aes_enc(rsa_encrypt.get_pub_key_auth(), payload), ((host.host, host.port)))

    #get auth to push user updates
    try:
        challenge()

    #exit if no auth
    except:
        return

#Grab the share for a given user from the local database
def grab(user):

    #initiate connection to the database
    conn = sqlite3.connect(settings.DBdir + settings.ID + ".db")
    conn.row_factory = sqlite3.Row
    c = conn.cursor()

    #Get the share associated to the provided user
    c.execute("SELECT * FROM shares WHERE id = ?", [user])
    inc = c.fetchone()

    #if no matching share then exit
    if inc == None:
        return -1

    #Close the connection and return the share
    conn.close()
    return inc

#authenticate the given user and key in order to send their share to the auth node
def auth_user(user, db,key):

    #Grab the user's share
    share = grab(user)

    #Return if error
    if share == -1:
        return

    if db == 'face':
        incoming_embed = string_to_embed(key)
        database_embed = string_to_embed(share["key"])
        dist = get_euclidean_distance(incoming_embed,database_embed)
        if dist <= settings.FACE_THRESH:
            send_share(share, Host())

    else:

        #Make sure that the given key matches the key in the database
        #and that the key is not null
        if key == share["key"] and not key == "":
            #Send the share if the key is valid
            send_share(share, Host())

#Recieves the user credentials from another process on the same host
#This should be sent to us from the web, qr, etc code
def recieve_creds(cli):

    #Recieve user:key
    try:
        #Grab key
        data = str(cli.recv(1024), 'ascii')

    #return if error
    except:
        cli.close()
        return

    #close the connection
    cli.close()

    #Split the data
    data = data.strip("\x0a").split(":")

    #make sure it is correct or return error
    if not len(data) == 2:
        return -1

    #Validate
    auth_user(data[0], settings.ID, data[1])
    return

#Start a listener on the localhost so that client programs can
#send user and  key information for validation
def start():

    #Create socket and bind it to localhost
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(("127.0.0.1", 55556))
        s.listen(5)

        #Recieve connections
        while 1 == 1:
            cli, addr = s.accept()

            #send connection to thread
            threading.Thread(target = recieve_creds, args = [cli]).start()