#!/usr/bin/env python3

import subprocess
import os.path
import time

from fract4d import fractconfig
from fract4d.tests import testbase

from fract4d_compiler import fc, translate


class Test(testbase.ClassSetup):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.g_comp.add_path("maps", fc.FormulaTypes.GRADIENT)
        cls.g_comp.load_formula_file("gf4d.frm")
        cls.g_comp.load_formula_file("gf4d.cfrm")
        cls.g_comp.load_formula_file("gf4d.cfrm")
        cls.g_comp.load_formula_file("gf4d.uxf")

    def tearDown(self):
        pass

    def testGetFileType(self):
        self.assertEqual(
            translate.T,
            Test.g_comp.guess_type_from_filename("x.frm"))
        self.assertEqual(
            translate.ColorFunc,
            Test.g_comp.guess_type_from_filename("x.ucl"))
        self.assertEqual(
            translate.Transform,
            Test.g_comp.guess_type_from_filename("x.uxf"))

    def testTypes(self):
        self.assertEqual(0, fc.FormulaTypes.FRACTAL)

    def assertListContains(self, list, element):
        try:
            return list.index(element)
        except ValueError as err:
            raise AssertionError(f"couldn't find {element} in {list}")

    def assertListDoesntContain(self, list, element):
        self.assertEqual(0, list.count(element))

    def testFindFilesOfType(self):
        expected_files = {
            fc.FormulaTypes.FRACTAL: "gf4d.frm",
            fc.FormulaTypes.COLORFUNC: "gf4d.cfrm",
            fc.FormulaTypes.TRANSFORM: "gf4d.uxf",
            fc.FormulaTypes.COLORFUNC: "standard.ucl",
            fc.FormulaTypes.GRADIENT: "blatte1.ugr",
            fc.FormulaTypes.GRADIENT: "4zebbowx.map"
        }

        for type in range(fc.FormulaTypes.FRACTAL,
                          fc.FormulaTypes.GRADIENT + 1):
            files = Test.g_comp.find_files_of_type(type)

            for (exp_t, exp_val) in list(expected_files.items()):
                if exp_t == type:
                    self.assertListContains(files, exp_val)
                else:
                    self.assertListDoesntContain(files, exp_val)

    def testLists(self):
        file = Test.g_comp.files["gf4d.cfrm"]
        names = file.get_formula_names()
        self.assertEqual(names, list(file.formulas.keys()))

        inside_names = file.get_formula_names("OUTSIDE")
        for f in inside_names:
            self.assertNotEqual(file.formulas[f].symmetry, "OUTSIDE")

        outside_names = file.get_formula_names("INSIDE")
        for f in outside_names:
            self.assertNotEqual(file.formulas[f].symmetry, "INSIDE")

    def testFileTimeChecking(self):
        'Check we notice when a file changes'
        f2 = fc.Compiler(Test.userConfig)

        formulas = '''
test_circle {
loop:
z = pixel
bailout:
|z| < @bailout
default:
float param bailout
	default = 4.0
endparam
}
test_square {
loop:
z = pixel
bailout: abs(real(z)) > 2.0 || abs(imag(z)) > 2.0
}
'''
        fftest_file = os.path.join(Test.tmpdir.name, "fttest.frm")
        with open(fftest_file, "w") as f:
            f.write(formulas)

        f2.load_formula_file(fftest_file)
        frm = f2.get_formula(fftest_file, "test_circle")
        self.assertEqual(frm.symbols.default_params(), [0, 4.0])

        formulas = formulas.replace('4.0', '6.0')
        time.sleep(1.0)  # ensure filesystem will have a different time
        with open(fftest_file, "w") as f:
            f.write(formulas)

        frm2 = f2.get_formula(fftest_file, "test_circle")
        self.assertEqual(frm2.symbols.default_params(), [0, 6.0])

    def testCompile(self):
        'Check we can compile a fractal and the resulting .so looks ok'
        ff = Test.g_comp.files["gf4d.frm"]
        self.assertNotEqual(ff.contents.index("Modified for Gf4D"), -1)
        self.assertNotEqual(ff.get_formula("T03-01-G4"), None)
        self.assertEqual(len(ff.formulas) > 0, 1)
        f = Test.g_comp.get_formula("gf4d.frm", "T03-01-G4")
        self.assertEqual(f.errors, [])
        test_out_file = os.path.join(Test.tmpdir.name, "test-out.so")
        cg = Test.g_comp.compile(f)
        Test.g_comp.generate_code(f, cg, test_out_file, None)
        # check the output contains the right functions
        (status, output) = subprocess.getstatusoutput('nm %s' % test_out_file)
        self.assertEqual(status, 0)
        self.assertEqual(output.count("pf_new"), 1)
        self.assertEqual(output.count("pf_calc"), 1)
        self.assertEqual(output.count("pf_init"), 1)
        self.assertEqual(output.count("pf_kill"), 1)

    def testErrors(self):
        'Check we raise appropriate exns when formulas are busted'
        self.assertRaises(
            IOError, Test.g_comp.load_formula_file, "nonexistent.frm")

        self.assertRaises(
            ValueError, Test.g_comp.get_formula, "test.xxx", "nonexistent")

        f = Test.g_comp.get_formula("test.frm", "nonexistent")
        self.assertEqual(f, None)
        f = Test.g_comp.get_formula("test.frm", "parse_error")
        self.assertEqual(len(f.errors), 1)

    def disabled_testEvil(self):
        # this was too slow so turned it off
        f = Test.g_comp.get_formula("test.frm", "ny2004-4")
        self.assertEqual(len(f.errors), 0)
        cg = Test.g_comp.compile(f)
        Test.g_comp.generate_code(f, cg, "test-evil.so", None)

        f = Test.g_comp.get_formula("test.frm", "Fractint-9-21")
        self.assertNoErrors(f)
        cg = Test.g_comp.compile(f)
        Test.g_comp.generate_code(f, cg, "test-evil.so", None)

    def testPreprocessor(self):
        f = Test.g_comp.get_formula("test.frm", "test_preprocessor")
        self.assertNoErrors(f)
        cg = Test.g_comp.compile(f)
        of = Test.g_comp.generate_code(f, cg)

    def testPreprocessorError(self):
        ff = Test.g_comp.load_formula_file("test_bad_pp.frm")
        f = Test.g_comp.get_formula("test_bad_pp.frm", "error")
        self.assertEqual(len(f.errors), 1)
        Test.g_comp.files["test_bad_pp.frm"] = None

    def testBehr(self):
        ff = Test.g_comp.get_file("Bujumbura.cs")
        ff = Test.g_comp.get_file("BehrQuiltedRed.cs")

    def testColorFunc(self):
        'Compile inner + outer colorfuncs and merge'
        cf1 = Test.g_comp.get_formula("gf4d.cfrm", "default", "cf0")
        self.assertEqual(len(cf1.errors), 0)
        Test.g_comp.compile(cf1)

        cf2 = Test.g_comp.get_formula("gf4d.cfrm", "zero", "cf1")
        self.assertEqual(len(cf2.errors), 0)
        Test.g_comp.compile(cf2)

        f = Test.g_comp.get_formula("gf4d.frm", "Mandelbrot")
        cg = Test.g_comp.compile(f)

        f.merge(cf1, "cf0_")
        f.merge(cf2, "cf1_")

        ofile = Test.g_comp.generate_code(f, cg)
        self.assertTrue(os.path.exists(ofile))

    def testDoubleCompile(self):
        'Compile the same thing twice, check results same'
        f = Test.g_comp.get_formula("gf4d.frm", "Mandelbrot")
        cg = Test.g_comp.compile(f)
        of1 = Test.g_comp.generate_code(f, cg)

        f2 = Test.g_comp.get_formula("gf4d.frm", "Mandelbrot")
        cg2 = Test.g_comp.compile(f2)
        of2 = Test.g_comp.generate_code(f, cg2)

        self.assertEqual(of1, of2)

    def testFormulasNotConnected(self):
        'fetch the same thing twice, check symbols tables differ'
        f = Test.g_comp.get_formula("fractint-builtin.frm", "julfn+exp")
        f2 = Test.g_comp.get_formula("fractint-builtin.frm", "julfn+exp")
        self.assertNotEqual(f, f2)
        self.assertNotEqual(f.symbols, f2.symbols)
        ol = f.symbols["@fn1"]
        ol2 = f2.symbols["@fn1"]
        self.assertNotEqual(ol, ol2)
        func = ol[0]
        func2 = ol2[0]
        self.assertNotEqual(func, func2)

    def testPrefs(self):
        compiler = fc.Compiler(Test.userConfig)
        prefs = fractconfig.T("testprefs")
        prefs.set("compiler", "name", "x")
        prefs.set("compiler", "options", "foo")
        prefs.set_list("formula_path", ["fish"])
        prefs.set_list("map_path", ["wibble"])

        compiler.update_from_prefs(prefs)

        self.assertEqual("x", compiler.compiler_name)
        self.assertEqual("foo", compiler.flags)
        self.assertEqual(["fish"], compiler.path_lists[0])
        self.assertEqual(["wibble"], compiler.path_lists[3])

    def testInstance(self):
        compiler = fc.Compiler(Test.userConfig)
        self.assertNotEqual(None, compiler)
        self.assertEqual(
            compiler.flags, Test.userConfig.get("compiler", "options"))

    def testAllFormulasCompile(self):
        'Go through every formula and check for errors'
        for filename in Test.g_comp.find_formula_files():
            ff = Test.g_comp.get_file(filename)
            for fname in ff.get_formula_names():
                # skip formulas expected to produce an error
                if fname.count("error") == 0:
                    f = Test.g_comp.get_formula(ff.filename, fname)
                    self.assertNoErrors(f, f"{filename}:{fname}")

    def testGetFormulaText(self):
        t = Test.g_comp.get_formula_text("gf4d.frm", "Mandelbrot")
        self.assertTrue(t.startswith("Mandelbrot {"))
