"""
Flitter engine functional tests
"""

import asyncio
from pathlib import Path
import sys
import tempfile
import unittest

import PIL.Image
import PIL.ImageChops
import PIL.ImageStat

from flitter.engine.control import EngineController


def image_diff(ref, img):
    """Return how dissimilar two images are as a score between 0 (identical) and 1 (completely different)"""
    return PIL.ImageStat.Stat(PIL.ImageChops.difference(ref, img).convert('L')).sum[0] / (img.width * img.height * 255)


TEST_IMAGES_DIR = Path(__file__).parent.parent / 'build/test_images'
TEST_IMAGES_DIR.mkdir(mode=0o775, parents=True, exist_ok=True)

for path in TEST_IMAGES_DIR.iterdir():
    if path.is_file():
        path.unlink(missing_ok=True)


class TestRendering(unittest.TestCase):
    """
    Some simple rendering functional tests

    Scripts will be executed for a single frame and a `SIZE` image should be
    saved to the filename `OUTPUT`.
    """

    SIZE = (200, 100)

    def setUp(self):
        self.script_path = Path(tempfile.mktemp('.fl'))
        self.input_path = Path(tempfile.mktemp('.png'))
        self.output_path = Path(tempfile.mktemp('.png'))
        self.controller = EngineController(realtime=False, offscreen=True, target_fps=1, run_time=1,
                                           defined_names={'SIZE': self.SIZE,
                                                          'INPUT': str(self.input_path),
                                                          'OUTPUT': str(self.output_path)})

    def tearDown(self):
        if self.script_path.exists():
            self.script_path.unlink()
        if self.input_path.exists():
            self.input_path.unlink()
        if self.output_path.exists():
            self.output_path.unlink()

    def test_simple_canvas(self):
        """Render a simple green rectangle filling the viewport"""
        self.script_path.write_text("""
!window size=SIZE
    !record filename=OUTPUT
        !canvas
            !path
                !rect size=SIZE
                !fill color=0;1;0
""", encoding='utf8', newline='\n')
        self.controller.load_page(self.script_path)
        asyncio.run(self.controller.run())
        img = PIL.Image.open(self.output_path)
        self.assertEqual(img.size, self.SIZE)
        self.assertEqual(img.tobytes(), b'\x00\xff\x00' * self.SIZE[0] * self.SIZE[1])

    def test_simple_canvas3d(self):
        """Render a simple green emissive box filling the viewport"""
        self.script_path.write_text("""
!window size=SIZE
    !record filename=OUTPUT
        !canvas3d orthographic=true
            !box size=SIZE*(1;1;0) emissive=0;1;0
""", encoding='utf8', newline='\n')
        self.controller.load_page(self.script_path)
        asyncio.run(self.controller.run())
        img = PIL.Image.open(self.output_path)
        self.assertEqual(img.size, self.SIZE)
        self.assertEqual(img.tobytes(), b'\x00\xff\x00' * self.SIZE[0] * self.SIZE[1])

    def test_simple_shader(self):
        """Render a simple shader that returns green for all fragments"""
        self.script_path.write_text("""
!window size=SIZE
    !record filename=OUTPUT
        !shader fragment='''#version 330
                            in vec2 coord;
                            out vec4 color;
                            void main() {
                                color = vec4(0, 1, 0, 1);
                            }'''
""", encoding='utf8', newline='\n')
        self.controller.load_page(self.script_path)
        asyncio.run(self.controller.run())
        img = PIL.Image.open(self.output_path)
        self.assertEqual(img.size, self.SIZE)
        self.assertEqual(img.tobytes(), b'\x00\xff\x00' * self.SIZE[0] * self.SIZE[1])

    def test_simple_image(self):
        """Render an image node from a simple temporary image"""
        PIL.Image.new('RGB', self.SIZE, (0, 255, 0)).save(self.input_path, 'PNG')
        self.script_path.write_text("""
!window size=SIZE
    !record filename=OUTPUT
        !image filename=INPUT
""", encoding='utf8', newline='\n')
        self.controller.load_page(self.script_path)
        asyncio.run(self.controller.run())
        img = PIL.Image.open(self.output_path)
        self.assertEqual(img.size, self.SIZE)
        self.assertEqual(img.tobytes(), b'\x00\xff\x00' * self.SIZE[0] * self.SIZE[1])


class ScriptTest(unittest.TestCase):
    def assertScriptOutputMatchesImage(self, script, suffix='.png', target_fps=1, run_time=1, **kwargs):
        output_path = TEST_IMAGES_DIR / script.with_suffix(suffix).name
        reference = PIL.Image.open(script.with_suffix('.png'))
        controller = EngineController(realtime=False, offscreen=True, defined_names={'OUTPUT': str(output_path)},
                                      target_fps=target_fps, run_time=run_time, **kwargs)
        controller.load_page(script)
        asyncio.run(controller.run())
        output = PIL.Image.open(output_path)
        self.assertEqual(reference.size, output.size, msg="Size mismatch")
        self.assertLess(image_diff(reference, output), 0.005, msg="Output differs from reference image")

    def assertAllScriptOutputsMatchesImages(self, scripts, suffix='.png', target_fps=1, run_time=1, **kwargs):
        for i, script in enumerate(scripts):
            with self.subTest(script=script, **kwargs):
                self.assertScriptOutputMatchesImage(script, suffix=suffix, target_fps=target_fps, run_time=run_time, **kwargs)


class TestDocumentationDiagrams(ScriptTest):
    """
    Recreate the documentation diagrams and check them against the pre-calculated ones.
    """

    DIAGRAMS = Path(__file__).parent.parent / 'docs/diagrams'

    def test_easings(self):
        self.assertScriptOutputMatchesImage(self.DIAGRAMS / 'easings.fl')

    def test_pseudorandoms(self):
        self.assertScriptOutputMatchesImage(self.DIAGRAMS / 'pseudorandoms.fl')

    def test_waveforms(self):
        self.assertScriptOutputMatchesImage(self.DIAGRAMS / 'waveforms.fl')


class TestDocumentationTutorial(ScriptTest):
    """
    Recreate the tutorial images and check them against the pre-calculated ones.
    """

    TUTORIAL_IMAGES = Path(__file__).parent.parent / 'docs/tutorial_images'

    def test_tutorial1(self):
        self.assertScriptOutputMatchesImage(self.TUTORIAL_IMAGES / 'tutorial1.fl')

    def test_tutorial2(self):
        self.assertScriptOutputMatchesImage(self.TUTORIAL_IMAGES / 'tutorial2.fl')

    def test_tutorial3(self):
        self.assertScriptOutputMatchesImage(self.TUTORIAL_IMAGES / 'tutorial3.fl')

    def test_tutorial4(self):
        self.assertScriptOutputMatchesImage(self.TUTORIAL_IMAGES / 'tutorial4.fl')

    def test_tutorial5(self):
        self.assertScriptOutputMatchesImage(self.TUTORIAL_IMAGES / 'tutorial5.fl')

    def test_tutorial6(self):
        self.assertScriptOutputMatchesImage(self.TUTORIAL_IMAGES / 'tutorial6.fl')


class TestExamples(ScriptTest):
    """
    Recreate the examples and check them against the pre-calculated ones.
    """

    EXAMPLES = Path(__file__).parent.parent / 'examples'

    def test_bauble(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'bauble.fl')

    def test_canvas3d(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'canvas3d.fl')

    def test_linelight(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'linelight.fl')

    def test_solidgeometry(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'solidgeometry.fl')

    def test_teaset(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'teaset.fl')

    def test_textures(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'textures.fl')

    def test_translucency(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'translucency.fl')

    def test_video(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'video.fl')

    def test_bounce(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'bounce.fl', target_fps=10)

    def test_hoops(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'hoops.fl', target_fps=10)

    def test_physics(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'physics.fl', target_fps=10)

    def test_smoke(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'smoke.fl', target_fps=10)

    @unittest.skipIf(sys.platform != 'linux', 'OpenGL ES only available on Linux')
    def test_bauble_opengl_es(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'bauble.fl', opengl_es=True, suffix='.es.png')

    @unittest.skipIf(sys.platform != 'linux', 'OpenGL ES only available on Linux')
    def test_canvas3d_opengl_es(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'canvas3d.fl', opengl_es=True, suffix='.es.png')

    @unittest.skipIf(sys.platform != 'linux', 'OpenGL ES only available on Linux')
    def test_linelight_opengl_es(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'linelight.fl', opengl_es=True, suffix='.es.png')

    @unittest.skipIf(sys.platform != 'linux', 'OpenGL ES only available on Linux')
    def test_solidgeometry_opengl_es(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'solidgeometry.fl', opengl_es=True, suffix='.es.png')

    @unittest.skipIf(sys.platform != 'linux', 'OpenGL ES only available on Linux')
    def test_teaset_opengl_es(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'teaset.fl', opengl_es=True, suffix='.es.png')

    @unittest.skipIf(sys.platform != 'linux', 'OpenGL ES only available on Linux')
    def test_textures_opengl_es(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'textures.fl', opengl_es=True, suffix='.es.png')

    @unittest.skipIf(sys.platform != 'linux', 'OpenGL ES only available on Linux')
    def test_translucency_opengl_es(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'translucency.fl', opengl_es=True, suffix='.es.png')

    @unittest.skipIf(sys.platform != 'linux', 'OpenGL ES only available on Linux')
    def test_bounce_opengl_es(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'bounce.fl', target_fps=10, opengl_es=True, suffix='.es.png')

    @unittest.skipIf(sys.platform != 'linux', 'OpenGL ES only available on Linux')
    def test_physics_opengl_es(self):
        self.assertScriptOutputMatchesImage(self.EXAMPLES / 'physics.fl', target_fps=10, opengl_es=True, suffix='.es.png')
