#!/usr/bin/env python3

import copy
import io
import math
import random
from time import time as now

from fract4d_compiler import fc, fctutils
from . import colorizer, formsettings, fract4dc, gradient

# the version of the earliest gf4d release which can parse all the files
# this version can output
THIS_FORMAT_VERSION = "3.10"

BLEND_NEAREST = 0
BLEND_FURTHEST = 1
BLEND_CW = 2
BLEND_CCW = 3


class T(fctutils.T):
    XCENTER = 0
    YCENTER = 1
    ZCENTER = 2
    WCENTER = 3
    MAGNITUDE = 4
    XYANGLE = 5
    XZANGLE = 6
    XWANGLE = 7
    YZANGLE = 8
    YWANGLE = 9
    ZWANGLE = 10

    FORMULA = 0
    OUTER = 1
    INNER = 2
    DEFAULT_FORMULA_FILE = "gf4d.frm"
    DEFAULT_FORMULA_FUNC = "Mandelbrot"
    paramnames = [
        "x",
        "y",
        "z",
        "w",
        "size",
        "xy",
        "xz",
        "xw",
        "yz",
        "yw",
        "zw"]

    def __init__(self, compiler, site=None):
        fctutils.T.__init__(self)

        self.format_version = 2.8

        self.bailfunc = 0
        # formula support
        self.forms = [
            formsettings.T(compiler, self),  # formula
            formsettings.T(compiler, self, "cf0"),  # outer
            formsettings.T(compiler, self, "cf1")  # inner
        ]

        self.transforms = []
        self.next_transform_id = 0
        self.compiler_options = {"optimize": 1}
        self.yflip = False
        self.periodicity = True
        self.period_tolerance = 1.0E-9
        self.auto_epsilon = False  # automatically set @epsilon param, if found
        self.auto_deepen = True  # automatically adjust maxiter
        self.auto_tolerance = True  # automatically adjust periodicity
        self.antialias = 1
        self.compiler = compiler
        self.outputfile = None
        self.render_type = 0
        self.pfunc = None
        self.handle = None

        self.warp_param = None
        # gradient

        # default is just white outside
        self.default_gradient = gradient.Gradient()
        self.default_gradient.segments[0].left_color = [1.0, 1.0, 1.0, 1.0]
        self.default_gradient.segments[0].right_color = [1.0, 1.0, 1.0, 1.0]

        # solid colors are black
        self.solids = [(0, 0, 0, 255), (0, 0, 0, 255)]

        # formula defaults
        self.set_formula(T.DEFAULT_FORMULA_FILE, T.DEFAULT_FORMULA_FUNC, 0)
        self.set_inner("gf4d.cfrm", "zero")
        self.set_outer("gf4d.cfrm", "continuous_potential")
        self.dirtyFormula = True  # formula needs recompiling
        self.dirty = True  # parameters have changed
        self.clear_image = True

        self.reset()

        # interaction with fract4dc library
        self.site = site or fract4dc.site_create(self)

        # colorfunc lookup
        self.colorfunc_names = [
            "default",
            "continuous_potential",
            "zero",
            "ejection_distance",
            "decomposition",
            "external_angle"]

        self.saved = True  # initial params not worth saving

    def serialize(self, comp=False):
        out = io.StringIO()
        self.save(out, False, compress=comp)
        return out.getvalue()

    def deserialize(self, string):
        self.loadFctFile(io.StringIO(string))
        self.changed()

    def apply_params(self, dict):
        for (key, value) in list(dict.items()):
            self.parseVal(key, value, None)

    def save(self, file, update_saved_flag=True, **kwds):
        print("gnofract4d parameter file", file=file)
        print("version=%s" % THIS_FORMAT_VERSION, file=file)

        compress = kwds.get("compress", False)
        if compress is not False:
            # compress this file
            main_file = file
            file = fctutils.Compressor()

        for pair in zip(self.paramnames, self.params):
            print("%s=%.17f" % pair, file=file)

        print("maxiter=%d" % self.maxiter, file=file)
        print("yflip=%s" % self.yflip, file=file)
        print("periodicity=%s" % int(self.periodicity), file=file)
        print("period_tolerance=%.17f" % self.period_tolerance, file=file)

        self.forms[0].save_formula_params(file, self.warp_param)
        self.forms[1].save_formula_params(file)
        self.forms[2].save_formula_params(file)

        i = 0
        for transform in self.transforms:
            transform.save_formula_params(file, None, i)
            i += 1

        print("[colors]", file=file)
        print("colorizer=1", file=file)
        print("solids=[", file=file)
        for solid in self.solids:
            print("%02x%02x%02x%02x" % solid, file=file)
        print("]", file=file)
        print("[endsection]", file=file)

        if compress:
            file.close()
            print(file.getvalue(), file=main_file)

        if update_saved_flag:
            self.saved = True

    def get_gradient(self):
        try:
            g = self.forms[0].get_named_param_value("@_gradient")
            if g == 0:
                g = self.default_gradient
        except BaseException:
            g = self.default_gradient
        return g

    def set_gradient(self, g):
        old_g = self.get_gradient()
        if old_g != g:
            self.forms[0].set_gradient(g)
            #needs_redraw = False
            # if self.forms[1].is_direct() or \
            #   self.forms[2].is_direct():
            #    needs_redraw = True

            self.changed(True)  # needs_redraw)

    def set_gradient_from_file(self, file, name):
        g = self.compiler.get_gradient(file, name)
        self.set_gradient(g)

    def parse_periodicity(self, val, f):
        try:
            self.set_periodicity(int(val))
        except ValueError:
            # might be a bool in 'True'/'False' format
            self.set_periodicity(bool(val))

    def parse_period_tolerance(self, val, f):
        self.period_tolerance = float(val)

    def set_period_tolerance(self, val):
        if val != self.period_tolerance:
            self.period_tolerance = val
            self.changed(False)

    def normalize_formulafile(self, params, formindex, formtype):
        formula = params.dict.get("formula")
        if formula:
            # we have an in-line formula, use that instead of
            # formulafile/function
            (formulafile, fname) = self.compiler.add_inline_formula(
                formula, formtype)
        else:
            formulafile = params.dict.get(
                "formulafile", self.forms[formtype].funcFile)
            fname = params.dict.get(
                "function", self.forms[formtype].funcName)

        return (formulafile, fname)

    def parse__inner_(self, val, f):
        params = fctutils.ParamBag()
        params.load(f)
        (file, func) = self.normalize_formulafile(
            params, 2, fc.FormulaTypes.COLORFUNC)
        self.set_inner(file, func)
        self.forms[2].load_param_bag(params)

    def parse__outer_(self, val, f):
        params = fctutils.ParamBag()
        params.load(f)
        (file, func) = self.normalize_formulafile(
            params, 1, fc.FormulaTypes.COLORFUNC)
        self.set_outer(file, func)
        self.forms[1].load_param_bag(params)

    def parse__function_(self, val, f):
        params = fctutils.ParamBag()
        params.load(f)
        (file, func) = self.normalize_formulafile(
            params, 0, fc.FormulaTypes.FRACTAL)

        self.set_formula(file, func, 0)

        for (name, val) in list(params.dict.items()):
            if name == "formulafile" or name == "function" or name == "formula" or name == "":
                continue
            elif name == "a" or name == "b" or name == "c":
                # back-compat for older versions
                self.forms[0].set_named_param("@" + name, val)
            else:
                self.forms[0].set_named_item(name, val)

    def parse__transform_(self, val, f):
        which_transform = int(val)
        params = fctutils.ParamBag()
        params.load(f)
        self.set_transform(
            params.dict["formulafile"],
            params.dict["function"],
            which_transform)
        self.transforms[which_transform].load_param_bag(params)

    def __del__(self):
        # print "deleting fractal %s" % self
        del self.pfunc
        del self.handle

    def __copy__(self):
        # override shallow-copy to do a deeper copy than normal,
        # but still don't try and copy *everything*

        c = T(self.compiler, self.site)

        c.maxiter = self.maxiter
        c.params = copy.copy(self.params)

        c.bailfunc = self.bailfunc

        for i in range(3):
            c.set_formula(self.forms[i].funcFile, self.forms[i].funcName, i)
            c.forms[i].copy_from(self.forms[i])

        for t in self.transforms:
            c.append_transform(t.funcFile, t.funcName)
            c.transforms[-1].copy_from(t)

        c.solids = copy.copy(self.solids)
        c.yflip = self.yflip
        c.periodicity = self.periodicity
        c.period_tolerance = self.period_tolerance
        c.auto_deepen = self.auto_deepen
        c.auto_tolerance = self.auto_tolerance
        c.saved = self.saved
        c.clear_image = self.clear_image
        c.warp_param = self.warp_param
        return c

    def determine_direction(self, a, b, mode):
        isClockwise = False

        if mode == BLEND_NEAREST:
            if abs(b - a) <= math.pi and a < b:
                isClockwise = True
            elif abs(b - a) > math.pi and a > b:
                isClockwise = True
        elif mode == BLEND_FURTHEST:
            if abs(b - a) <= math.pi and a > b:
                isClockwise = True
            if abs(b - a) > math.pi and a < b:
                isClockwise = True
        elif mode == BLEND_CW:
            isClockwise = True
        elif mode == BLEND_CCW:
            isClockwise = False
        else:
            raise ValueError("Unknown angle blend mode %s" % mode)

        return isClockwise

    def blend_angle(self, a, b, ratio, mode):
        isClockwise = self.determine_direction(a, b, mode)

        if isClockwise and b < a:
            b = b + math.pi * 2.0
        if not isClockwise and b > a:
            a = a + math.pi * 2.0

        angle = a * (1 - ratio) + b * ratio
        while angle > math.pi:
            angle -= math.pi * 2.0

        return angle

    def blend(self, other, ratio, angle_options=()):
        """Create a new fractal which blends this and the other's parameter sets using ratio.
        'angle_options' can be used to override the default method of interpolating angles."""
        new = copy.copy(self)
        for i in range(self.XCENTER, self.MAGNITUDE):
            a, b = self.params[i], other.params[i]
            new.set_param(i, a * (1 - ratio) + b * ratio)

        # magnitude is exponential
        a, b = self.params[self.MAGNITUDE], other.params[self.MAGNITUDE]
        if abs(a) > abs(b):
            factor = (a - b) / (math.e - 1)
            val = factor * math.exp(1 - ratio) + (b - factor)
        else:
            factor = (b - a) / (math.e - 1)
            val = factor * math.exp(ratio) + (a - factor)

        new.set_param(self.MAGNITUDE, val)

        for i in range(self.XYANGLE, self.ZWANGLE + 1):
            a, b = self.params[i], other.params[i]
            option = angle_options[i - self.XYANGLE:i - self.XYANGLE]
            if option:
                mode = option[0]
            else:
                mode = BLEND_NEAREST
            new.set_param(i, self.blend_angle(a, b, ratio, mode))

        for form1, form2 in zip(new.forms, other.forms):
            form1.blend(form2, ratio)

        return new

    def reset_angles(self):
        for i in range(self.XYANGLE, self.ZWANGLE + 1):
            self.set_param(i, 0.0)

    def reset(self):
        # set global default values, then override from formula
        # set up defaults
        self.params = [
            0.0, 0.0, 0.0, 0.0,  # center
            4.0,  # size
            0.0, 0.0, 0.0, 0.0, 0.0, 0.0  # angles
        ]

        self.bailout = 0.0
        self.maxiter = 256
        self.rot_by = math.pi / 2
        self.title = self.forms[0].funcName
        self.yflip = False
        self.auto_epsilon = False
        self.period_tolerance = 1.0E-9

        g = self.get_gradient()
        self.set_formula_defaults(g)

    def reset_zoom(self):
        mag = self.forms[0].formula.defaults.get("magn")
        if mag:
            mag = mag.value
        else:
            mag = self.forms[0].formula.defaults.get("magnitude")
            if mag:
                mag = mag.value
            else:
                mag = 4.0
        self.set_param(self.MAGNITUDE, mag)

    def copy_colors(self, f):
        self.set_gradient(copy.copy(f.get_gradient()))
        self.set_solids(f.solids)
        self.changed(False)

    def set_warp_param(self, param):
        if self.warp_param != param:
            self.warp_param = param
            self.changed(True)

    def set_cmap(self, mapfile):
        c = colorizer.T(self)
        with open(mapfile) as file:
            c.parse_map_file(file)
        self.set_gradient(c.gradient)
        self.set_solids(c.solids)
        self.changed(False)

    def get_initparam(self, n, param_type):
        params = self.forms[param_type].params
        return params[n]

    def set_initparam(self, n, val, param_type):
        self.forms[param_type].set_param(n, val)

    def set_solid(self, i, newsolid):
        if self.solids[i] == newsolid:
            return
        self.solids[i] = newsolid
        self.changed(False)

    def set_solids(self, solids):
        same = True
        for i in range(len(solids)):
            if self.solids[i] != solids[i]:
                same = False
                break
        if same:
            return

        self.solids[0:len(solids)] = solids[:]
        self.changed(False)

    def refresh(self):
        for i in range(3):
            if self.compiler.out_of_date(self.forms[i].funcFile):
                self.set_formula(
                    self.forms[i].funcFile, self.forms[i].funcName, i)

    def set_formula_defaults(self, g=None):
        if self.forms[0].formula is None:
            return

        if g is None:
            g = self.get_gradient()

        self.forms[0].set_initparams_from_formula(g)

        for (name, val) in list(self.forms[0].formula.defaults.items()):
            # FIXME helpfile,helptopic,method,precision,
            # render,skew,stretch
            if name == "maxiter":
                self.maxiter = int(val.value)
            elif name == "center" or name == "xycenter":
                self.params[self.XCENTER] = float(val.value[0].value)
                self.params[self.YCENTER] = float(val.value[1].value)
            elif name == "zwcenter":
                self.params[self.ZCENTER] = float(val.value[0].value)
                self.params[self.WCENTER] = float(val.value[1].value)
            elif name == "angle":
                self.params[self.XYANGLE] = float(val.value)
            elif name == "magn":
                self.params[self.MAGNITUDE] = float(val.value)
            elif name == "title":
                self.title = val.value
            elif name == "periodicity":
                self.periodicity = int(val.value)
            else:
                if hasattr(self, name.upper()):
                    self.params[getattr(self, name.upper())] = float(val.value)
                else:
                    print("ignored unknown parameter %s" % name)

        for form in self.forms[1:] + self.transforms:
            form.reset_params()

    def set_formula(self, formulafile, func, index=0):
        self.forms[index].set_formula(formulafile, func, self.get_gradient())

        if index == 0:
            self.set_bailfunc()
            self.warp_param = None
        self.formula_changed()
        self.changed()

    def get_saved(self):
        return self.saved

    def set_bailfunc(self):
        bailfuncs = [
            "cmag", "manhattanish", "manhattanish2",
            "max2", "min2",
            "real2", "imag2",
            None  # bailout
        ]
        funcname = bailfuncs[self.bailfunc]
        if funcname is None:
            # FIXME deal with diff
            return

        func = self.forms[0].formula.symbols.get("@bailfunc")
        if func is not None:
            self.set_func(func[0], funcname, self.forms[0].formula)

    def changed(self, clear_image=True):
        self.dirty = True
        self.saved = False
        self.clear_image = clear_image

    def formula_changed(self):
        self.dirtyFormula = True

    def set_func(self, func, fname, formula):
        if func.cname != fname:
            formula.symbols.set_std_func(func, fname)
            self.dirtyFormula = True
            self.changed()

    def set_periodicity(self, periodicity):
        if self.periodicity != periodicity:
            self.periodicity = periodicity
            self.changed()

    def set_inner(self, funcfile, funcname):
        self.set_formula(funcfile, funcname, 2)

    def set_outer(self, funcfile, funcname):
        self.set_formula(funcfile, funcname, 1)

    def get_transform_prefix(self):
        i = self.next_transform_id
        prefix = "t%d" % i
        self.next_transform_id += 1
        return prefix

    def append_transform(self, funcfile, funcname):
        fs = formsettings.T(self.compiler, self, self.get_transform_prefix())
        fs.set_formula(funcfile, funcname, self.get_gradient())
        self.transforms.append(fs)
        self.formula_changed()
        self.changed()

    def set_transform(self, funcfile, funcname, i):
        fs = formsettings.T(self.compiler, self, self.get_transform_prefix())
        fs.set_formula(funcfile, funcname, self.get_gradient())
        if len(self.transforms) <= i:
            self.transforms.extend([None] * (i - len(self.transforms) + 1))

        self.transforms[i] = fs
        self.formula_changed()
        self.changed()

    def remove_transform(self, i):
        self.transforms.pop(i)
        self.formula_changed()
        self.changed()

    def set_compiler_option(self, option, val):
        self.compiler_options[option] = val
        self.dirtyFormula = True

    def apply_options(self, options):
        if options.formula.name and options.formula.func:
            self.set_formula(options.formula.name, options.formula.func)
            self.reset()

        if options.inner.name and options.inner.func:
            self.set_formula(options.inner.name, options.inner.func)
            self.reset()

        if options.outer.name and options.outer.func:
            self.set_formula(options.outer.name, options.outer.func)
            self.reset()

        if options.maxiter != -1:
            self.set_maxiter(options.maxiter)

        for num, val in options.paramchanges.items():
            self.set_param(num, val)

        for t in options.transforms:
            self.append_transform(t.name, t.func)

        if options.map:
            self.set_cmap(options.map)

        if options.antialias is not None:
            self.antialias = options.antialias

    def compile(self):
        if self.forms[0].formula is None:
            raise ValueError("no formula")
        if self.dirtyFormula is False:
            return self.outputfile

        outputfile = self.compiler.compile_all(
            self.forms[0].formula,
            self.forms[1].formula,
            self.forms[2].formula,
            [x.formula for x in self.transforms],
            self.compiler_options)

        if outputfile is not None:
            self.set_output_file(outputfile)

        self.dirtyFormula = False
        return self.outputfile

    def set_output_file(self, outputfile):
        if self.outputfile != outputfile:
            self.outputfile = outputfile
            self.handle = fract4dc.pf_load(self.outputfile)
            self.pfunc = fract4dc.pf_create(self.handle)

    def make_random_colors(self, n):
        self.get_gradient().randomize(n)
        self.changed(False)

    def mul_vs(self, v, s):
        return [x * s for x in v]

    def xy_random(self, weirdness, size):
        return weirdness * 0.5 * size * (random.random() - 0.5)

    def zw_random(self, weirdness, size):
        factor = math.fabs(1.0 - math.log(size)) + 1.0
        return weirdness * (random.random() - 0.5) * 1.0 / factor

    def angle_random(self, weirdness):
        action = random.random()
        if action > weirdness:
            return 0.0  # no change

        action = random.random()
        if action < weirdness / 6.0:
            # +/- pi/2
            if random.random() > 0.5:
                return math.pi / 2.0
            else:
                return math.pi / 2.0

        return weirdness * (random.random() - 0.5) * math.pi / 2.0

    def is4D(self):
        return self.warp_param is not None or self.forms[0].formula.is4D()

    def mutate(self, weirdness, color_weirdness):
        '''randomly adjust position, colors, angles and parameters.
        weirdness is between 0 and 1 - 0 is no change, 1 is lots'''

        size = self.params[self.MAGNITUDE]
        self.params[self.XCENTER] += self.xy_random(weirdness, size)
        self.params[self.YCENTER] += self.xy_random(weirdness, size)

        self.params[self.XYANGLE] += self.angle_random(weirdness)

        if self.is4D():
            self.params[self.ZCENTER] += self.zw_random(weirdness, size)
            self.params[self.WCENTER] += self.zw_random(weirdness, size)

            for a in range(self.XZANGLE, self.ZWANGLE):
                self.params[a] += self.angle_random(weirdness)

        if random.random() < weirdness * 0.75:
            self.params[self.MAGNITUDE] *= 1.0 + (0.5 - random.random())

        for f in self.forms:
            f.mutate(weirdness, size)

        if random.random() < color_weirdness:
            try:
                (file, formula) = self.compiler.get_random_gradient()
                self.set_gradient_from_file(file, formula)
            except IndexError:
                # can occur if no gradients available or occasionally
                # because random.choice is horked
                pass

    def nudge(self, x, y, axis=0):
        # move a little way in x or y
        self.relocate(0.025 * x, 0.025 * y, 1.0, axis)

    def get_form(self, param_type):
        if param_type > 2:
            form = self.transforms[param_type - 3]
        else:
            form = self.forms[param_type]
        return form

    def nudge_param(self, i, param_type, x, y):
        form = self.get_form(param_type)
        form.nudge_param(i, x, y)

    def relocate(self, dx, dy, zoom, axis=0):
        if dx == 0 and dy == 0 and zoom == 1.0:
            return

        m = fract4dc.rot_matrix(self.params)

        deltax = self.mul_vs(m[axis], dx)
        if self.yflip:
            deltay = self.mul_vs(m[axis + 1], dy)
        else:
            deltay = self.mul_vs(m[axis + 1], -dy)

        self.params[self.XCENTER] += deltax[0] + deltay[0]
        self.params[self.YCENTER] += deltax[1] + deltay[1]
        self.params[self.ZCENTER] += deltax[2] + deltay[2]
        self.params[self.WCENTER] += deltax[3] + deltay[3]
        self.params[self.MAGNITUDE] *= zoom
        self.changed()

    def flip_to_julia(self):
        self.params[self.XZANGLE] += self.rot_by
        self.params[self.YWANGLE] += self.rot_by
        self.rot_by = - self.rot_by
        self.changed()

    # status callbacks
    def status_changed(self, val):
        pass

    def progress_changed(self, d):
        pass

    def stats_changed(self, s):
        pass

    def is_interrupted(self):
        return False

    def iters_changed(self, iters):
        # print "iters changed to %d" % iters
        self.maxiter = iters

    def tolerance_changed(self, tolerance):
        # print "tolerance changed to %g" % tolerance
        self.period_tolerance = tolerance

    def image_changed(self, x1, y1, x2, y2):
        pass

    # you need to rename this to pixel_changed to get pixel debug info
    def _pixel_changed(self, params, x, y, aa, maxIters,
                       nNoPeriodIters, dist, fate, nIters, r, g, b, a):
        # remove underscore to debug fractal generation
        print("pixel: (%g,%g,%g,%g) %d %d %d %d %d %g %d %d (%d %d %d %d)" %
              (params[0], params[1], params[2], params[3], x, y, aa, maxIters, nNoPeriodIters, dist, fate, nIters, r, g, b, a))

    def epsilon_tolerance(self, w, h):
        # 5% of the size of a pixel
        return self.params[self.MAGNITUDE] / (20.0 * max(w, h))

    def all_params(self):
        p = []
        for form in self.forms:
            p += form.params
        for transform in self.transforms:
            p += transform.params
        return p

    def get_colormap(self):
        cmap = fract4dc.cmap_create_gradient(self.get_gradient().segments)

        (r, g, b, a) = self.solids[0]
        fract4dc.cmap_set_solid(cmap, 0, r, g, b, a)
        (r, g, b, a) = self.solids[1]
        fract4dc.cmap_set_solid(cmap, 1, r, g, b, a)

        return cmap

    def init_pfunc(self):
        initparams = self.all_params()
        fract4dc.pf_init(self.pfunc, self.params, initparams)

    def get_warp(self):
        if self.warp_param:
            warp = self.forms[0].order_of_name(self.warp_param)
        else:
            warp = -1
        return warp

    def set_maxiter(self, new_iter):
        if self.maxiter != new_iter:
            self.maxiter = new_iter
            self.changed(False)

    def set_antialias(self, aa):
        if aa != self.antialias:
            self.antialias = aa
            self.changed(True)

    def set_auto_deepen(self, auto_deepen):
        if auto_deepen != self.auto_deepen:
            self.auto_deepen = auto_deepen
            self.changed(True)

    def set_auto_tolerance(self, auto_tolerance):
        if auto_tolerance != self.auto_tolerance:
            self.auto_tolerance = auto_tolerance
            self.changed(True)

    def calc(self, image, colormap, nthreads, site, asynchronous):
        fract4dc.calc(
            params=self.params,
            antialias=self.antialias,
            maxiter=self.maxiter,
            yflip=self.yflip,
            periodicity=self.periodicity,
            nthreads=nthreads,
            pfo=self.pfunc,
            cmap=colormap,
            auto_deepen=self.auto_deepen,
            auto_tolerance=self.auto_tolerance,
            tolerance=self.period_tolerance,
            render_type=self.render_type,
            warp_param=self.get_warp(),
            image=image._img,
            site=site,
            dirty=self.clear_image,
            asynchronous=asynchronous)

    def drawpoint(self, repeats=1000):
        self.init_pfunc()
        print("x:\t\t%.17f\ny:\t\t%.17f\nz:\t\t%.17f\nw:\t\t%.17f\n" %
              tuple(self.params[0:4]))
        startTime = now()
        result = fract4dc.pf_calc(
            self.pfunc, self.params[0:4], self.maxiter, 0, 0, 0, repeats)
        duration = now() - startTime
        print(
            "iterations:\t%s\nfate:\t\t%s\ndistance:\t%s\nsolid:\t\t%s" %
            result)
        print("duration:\t%.4g" % duration)

    def draw(self, image, nthreads=1):
        self.init_pfunc()

        colormap = self.get_colormap()
        for (xoff, yoff, xres, yres) in image.get_tile_list():
            image.resize_tile(xres, yres)
            image.set_offset(xoff, yoff)

            self.calc(image, colormap, nthreads, self.site, False)

            image.save_tile()

    def clean(self):
        self.dirty = False

    def set_param(self, n, val):
        val = float(val)
        if self.params[n] != val:
            self.params[n] = val
            self.changed()

    def get_param(self, n):
        return self.params[n]

    def parse_gnofract4d_parameter_file(self, val, f):
        pass

    def parse_version_string(self, s):
        try:
            (major, minor) = tuple([int(a) for a in s.split(".")])
            return major * 1000.0 + minor
        except BaseException:
            raise ValueError("Invalid version number %s" % s)

    def parse_version(self, val, f):
        global THIS_FORMAT_VERSION
        self.format_version = self.parse_version_string(val)
        this_format_version = self.parse_version_string(THIS_FORMAT_VERSION)
        if self.format_version < 2000.0:
            # old versions displayed everything upside down
            # switch the rotation so they load OK
            self.yflip = True
        if 1700.0 < self.format_version < 2000.0:
            # a version that used auto-tolerance for Nova and Newton
            self.auto_epsilon = True

        if self.format_version > this_format_version:
            warning = \
                '''This file was created by a newer version of Gnofract 4D.
The image may not display correctly. Please upgrade to version %s or higher.'''

            self.warn(warning % val)

    def warn(self, msg):
        print(msg)

    def parse_bailfunc(self, val, f):
        # can't set function directly because formula hasn't been parsed yet
        self.bailfunc = int(val)

    def apply_colorizer(self, cf):
        if cf.read_gradient:
            self.set_gradient(cf.gradient)
        self.set_solids(cf.solids)
        self.changed(False)
        if cf.direct:
            # loading a legacy rgb colorizer
            self.set_outer("gf4d.cfrm", "rgb")

            val = "(%f,%f,%f,1.0)" % tuple(cf.rgb)
            self.forms[1].set_named_item("@col", val)

    def parse__colors_(self, val, f):
        cf = colorizer.T(self)
        cf.load(f)
        self.apply_colorizer(cf)

    def parse__colorizer_(self, val, f):
        which_cf = int(val)
        cf = colorizer.T(self)
        cf.load(f)
        if which_cf == 0:
            self.apply_colorizer(cf)
        # ignore other colorlists for now

    def parse_inner(self, val, f):
        name = self.colorfunc_names[int(val)]
        self.set_inner("gf4d.cfrm", name)

    def parse_outer(self, val, f):
        name = self.colorfunc_names[int(val)]
        self.set_outer("gf4d.cfrm", name)

    def set_yflip(self, yflip):
        if self.yflip != yflip:
            self.yflip = yflip
            self.changed()

    def parse_yflip(self, val, f):
        self.yflip = (val == "1" or val == "True")

    def parse_x(self, val, f):
        self.set_param(self.XCENTER, val)

    def parse_y(self, val, f):
        self.set_param(self.YCENTER, val)

    def parse_z(self, val, f):
        self.set_param(self.ZCENTER, val)

    def parse_w(self, val, f):
        self.set_param(self.WCENTER, val)

    def parse_size(self, val, f):
        self.set_param(self.MAGNITUDE, val)

    def parse_xy(self, val, f):
        self.set_param(self.XYANGLE, val)

    def parse_xz(self, val, f):
        self.set_param(self.XZANGLE, val)

    def parse_xw(self, val, f):
        self.set_param(self.XWANGLE, val)

    def parse_yz(self, val, f):
        self.set_param(self.YZANGLE, val)

    def parse_yw(self, val, f):
        self.set_param(self.YWANGLE, val)

    def parse_zw(self, val, f):
        self.set_param(self.ZWANGLE, val)

    def parse_bailout(self, val, f):
        self.bailout = float(val)

    def parse_maxiter(self, val, f):
        self.maxiter = int(val)

    def parse_antialias(self, val, f):
        # antialias now a user pref, not saved in file
        #self.antialias = int(val)
        pass

    def order_of_name(self, name, symbol_table):
        op = symbol_table.order_of_params()
        rn = symbol_table.mangled_name(name)
        ord = op.get(rn)
        if ord is None:
            # print "can't find %s (%s) in %s" % (name,rn,op)
            pass
        return ord

    def fix_bailout(self):
        # because bailout occurs before we know which function this is
        # in older files, we save it in self.bailout then apply to the
        # initparams later
        if self.bailout != 0.0:
            for f in self.forms:
                f.try_set_named_item("@bailout", self.bailout)

    def fix_gradients(self, old_gradient):
        # new gradient is read in after the gradient params have been set,
        # so this is needed to fix any which are using that default
        p = self.forms[0].params
        for i in range(len(p)):
            if p[i] == old_gradient:
                p[i] = self.get_gradient()

    def param_display_name(self, name, param):
        if hasattr(param, "title"):
            return param.title.value
        if hasattr(param, "caption"):
            return param.caption.value
        if name[:5] == "t__a_":
            name = name[5:]
        return name.title()

    def param_tip(self, name, param):
        if hasattr(param, "hint"):
            return param.hint.value
        return self.param_display_name(name, param)

    def loadFctFile(self, f):
        #old_gradient = self.get_gradient()
        line = f.readline()
        if line is None or not line.startswith("gnofract4d parameter file"):
            raise Exception("Not a valid parameter file")

        self.load(f)

        self.fix_bailout()
        # self.fix_gradients(old_gradient)
        self.saved = True
