#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  widgets.py
#
#  Copyright 2016 notna <notna@apparat.org>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#
#

__all__ = [
    "BasicWidget","Background",
    "Widget","EmptyBackground",
    ]

import weakref
import time

# Internal Debug/Performance monitor variable
_num_saved_redraws = 0

try:
    import pyglet
    from pyglet.gl import *
except ImportError:
    pass # Headless mode

from ..util import mouse_aabb, ActionDispatcher, WatchingList as _WatchingList

class Background(object):
    """
    Class representing the background of a widget.

    This base class does not do anything.
    """
    def __init__(self,widget):
        self.widget = widget
        self.initialized = False
        self._vlists = []
    def init_bg(self):
        """
        Called just before the background will be drawn the first time.

        Commonly used to initialize vertex lists.

        It is recommended to add all vertex lists to the ``submenu.batch2d`` Batch to speed up rendering and preventing glitches with grouping.
        """
        pass
    def redraw_bg(self):
        """
        Method called by the parent widget every time its :py:meth:`Widget.redraw()` method is called.
        """
        pass

    def reg_vlist(self,vlist):
        """
        Registers a vertex list to the internal list.

        This allows the class to clean itself up properly during deletion, as the background would still be visible after deletion otherwise.
        """
        self._vlists.append(vlist)

    @property
    def submenu(self):
        """
        Property for accessing the parent widget's submenu.
        """
        return self.widget.submenu

    @property
    def window(self):
        """
        Property for accessing the parent widget's window.
        """
        return self.widget.window

    @property
    def peng(self):
        """
        Property for accessing the parent widget's instance of :py:class:`peng3d.peng.Peng`\ .
        """
        return self.widget.peng

    @property
    def pressed(self):
        """
        Read-only helper property for easier access.

        Equivalent to ``widget.pressed``\ .
        """
        return self.widget.pressed
    @property
    def is_hovering(self):
        """
        Read-only helper property for easier access.

        Equivalent to ``widget.is_hovering``\ .
        """
        return self.widget.is_hovering

    def __del__(self):
        for vlist in self._vlists:
            try:
                vlist.delete()
            except Exception:
                pass
        self._vlists=[]

class EmptyBackground(Background):
    """
    Background that draws simply nothing.

    Can be used as a placeholder.
    """
    pass

class BasicWidget(ActionDispatcher):
    """
    Basic Widget class.

    Every widget must be registered with their appropriate sub-menus to work properly.

    ``pos`` may be either a list or 2-tuple of ``(x,y)`` for static positions or a function with the signature ``window_width,window_height,widget_width,widget_height`` returning a tuple.

    ``size`` is similar to ``pos`` but will only get ``window_width,window_height`` as its arguments.

    Commonly, the lambda ``lambda sw,sh,bw,bh: (sw/2.-bw/2.,sh/2.-bh/2.)`` is used to center the widget.

    The actions available may differ from widget to widget, by default these are used:

    - ``press`` is called upon starting to click on the widget
    - ``click`` is called if the mouse is released on the widget while also having been pressed on it before, recommended for typical button callbacks
    - ``context`` is called upon right-clicking on the widget and may be used to display a context menu
    - ``hover_start`` signals that the cursor is now hovering over the widget
    - ``hover`` is called every time the cursor moves while still being over the widget
    - ``hover_end`` is called after the cursor leaves the widget

    """
    def __init__(self,name,submenu,window,peng,
                 pos=None,size=None):
        self.name = name
        self.submenu = submenu
        self.window = window
        self.peng = peng

        self.actions = {}

        self._pos = pos
        self._size = size

        self.is_hovering = False
        self.pressed = False
        self._enabled = True
        self._is_scrollbar = False
        self.do_redraw = True

        self.registerEventHandlers()

    def registerEventHandlers(self):
        """
        Registers event handlers used by this widget, e.g. mouse click/motion and window resize.

        This will allow the widget to redraw itself upon resizing of the window in case the position needs to be adjusted.
        """
        self.peng.registerEventHandler("on_mouse_press",self.on_mouse_press)
        self.peng.registerEventHandler("on_mouse_release",self.on_mouse_release)
        self.peng.registerEventHandler("on_mouse_drag",self.on_mouse_drag)
        self.peng.registerEventHandler("on_mouse_motion",self.on_mouse_motion)
        self.peng.registerEventHandler("on_resize",self.on_resize)

    @property
    def pos(self):
        """
        Property that will always be a 2-tuple representing the position of the widget.

        Note that this method may call the method given as ``pos`` in the initializer.

        The returned object will actually be an instance of a helper class to allow for setting only the x/y coordinate.

        This property also respects any :py:class:`Container` set as its parent, any offset will be added automatically.

        Note that setting this property will override any callable set permanently.
        """
        if isinstance(self._pos,list) or isinstance(self._pos,tuple):
            r = self._pos
        elif callable(self._pos):
            w,h = self.submenu.size[:]
            r = self._pos(w,h,*self.size)
        else:
            raise TypeError("Invalid position type")

        ox,oy = self.submenu.pos
        r = r[0]+ox,r[1]+oy

        #if isinstance(self.submenu,ScrollableContainer) and not self._is_scrollbar:# and self.name != "__scrollbar_%s"%self.submenu.name: # Widget inside scrollable container and not the scrollbar
        #    r = r[0],r[1]+self.submenu.offset_y
        return _WatchingList(r,self._wlredraw_pos)
    @pos.setter
    def pos(self,value):
        self._pos = value
        self.redraw()

    @property
    def size(self):
        """
        Similar to :py:attr:`pos` but for the size instead.
        """
        if isinstance(self._size,list) or isinstance(self._size,tuple):
            s = self._size
        elif callable(self._size):
            w,h = self.submenu.size[:]
            s = self._size(w,h)
        else:
            raise TypeError("Invalid size type")

        s = s[:]

        if s[0]==-1:
            s[0]=self.getMinSize()[0]
        if s[1]==-1:
            s[1]=self.getMinSize()[1]

        # Prevents crashes with negative size
        s = [max(s[0],0),max(s[1],0)]
        return _WatchingList(s,self._wlredraw_size)
    @size.setter
    def size(self,value):
        self._size = value
        self.redraw()

    @property
    def clickable(self):
        """
        Property used for determining if the widget should be clickable by the user.

        This is only true if the submenu of this widget is active and this widget is enabled.

        The widget may be either disabled by setting this property or the :py:attr:`enabled` attribute.
        """
        if not isinstance(self.submenu,Container):
            return self.submenu.name == self.submenu.menu.activeSubMenu and self.submenu.menu.name == self.window.activeMenu and self.enabled
        else:
            return self.submenu.clickable and self.enabled
    @clickable.setter
    def clickable(self,value):
        self._enabled=value
        self.redraw()

    @property
    def enabled(self):
        """
        Property used for storing whether or not this widget is enabled.

        May influence rendering and behavior.

        Note that the widget will be immediately redrawn if this property is changed.
        """
        return self._enabled
    @enabled.setter
    def enabled(self,value):
        self._enabled=value
        self.redraw()

    def getState(self):
        """
        Returns the current state of the widget.

        One of ``"pressed"``\ , ``"hover"``\ , ``"disabled"`` or ``"idle"``\ .
        Note that some information may be lost by getting this state,
        for example it is not possible to know if the widget is hovered or not
        if ``"pressed"`` is returned. However, this should not be a problem for
        most intended uses of this method.
        """
        # Does not convey all information, loses some
        if self.pressed:
            return "pressed"
        elif self.is_hovering:
            return "hover"
        elif not self.enabled:
            return "disabled"
        else:
            return "idle"

    def getMinSize(self):
        raise NotImplementedError("%s does not support perfect-size widgets")
    def draw(self):
        """
        Draws all vertex lists associated with this widget.
        """
        if self.do_redraw:
            self.on_redraw()
            self.do_redraw = False
    def redraw(self):
        """
        Triggers a redraw of the widget.

        Note that the redraw may not be executed instantly, but rather batched together on the next frame.
        If an instant and synchronous redraw is needed, use :py:meth:`on_redraw()` instead.
        """
        # Uncomment if you want to see how many unneccessary redraws have been prevented
        # Warning: may cause lots of console spam and thus inaccurate results
        #if self.do_redraw:
        #    # Debug - not for production use
        #    global _num_saved_redraws
        #    _num_saved_redraws+=1
        #    print("saved redraw #%s"%_num_saved_redraws)
        self.do_redraw = True
    def on_redraw(self):
        """
        Callback to be overridden by subclasses called if redrawing the widget seems necessary.

        Note that this method should not be called manually, see :py:meth:`redraw()` instead.
        """
        pass

    def _wlredraw_pos(self,wl):
        self._pos = wl[:]
        self.redraw()
    def _wlredraw_size(self,wl):
        self._size = wl[:]
        self.redraw()

    def on_mouse_press(self,x,y,button,modifiers):
        if not self.clickable:
            return
        elif mouse_aabb([x,y],self.size,self.pos) and mouse_aabb([x,y],self.submenu.size,self.submenu.pos):
            if button == pyglet.window.mouse.LEFT:
                self.doAction("press")
                self.pressed = True
                self.doAction("statechanged")
            elif button == pyglet.window.mouse.RIGHT:
                self.doAction("context")
            self.redraw()
    def on_mouse_release(self,x,y,button,modifiers):
        if not self.clickable:
            return
        if self.pressed:
            if mouse_aabb([x,y],self.size,self.pos):
                self.doAction("click")
            self.pressed = False
            self.doAction("statechanged")
            self.redraw()
    def on_mouse_drag(self,x,y,dx,dy,button,modifiers):
        self.on_mouse_motion(x,y,dx,dy)
    def on_mouse_motion(self,x,y,dx,dy):
        if not self.clickable:
            return
        if mouse_aabb([x,y],self.size,self.pos):
            if not self.is_hovering:
                self.is_hovering = True
                self.doAction("hover_start")
                self.doAction("statechanged")
                self.redraw()
            else:
                self.doAction("hover")
        else:
            if self.is_hovering:
                self.is_hovering = False
                self.doAction("hover_end")
                self.doAction("statechanged")
                self.redraw()
    def on_resize(self,width,height):
        self.redraw()

    def delete(self):
        """
        Deletes resources of this widget that require manual cleanup.

        Currently removes all actions, event handlers and the background.

        The background itself should automatically remove all vertex lists to avoid visual artifacts.

        Note that this method is currently experimental, as it seems to have a memory leak.
        """
        # TODO: fix memory leak upon widget deletion
        del self.bg.widget
        del self.bg

        #self.clickable=False

        del self._pos
        del self._size

        self.actions = {}

        for e_type,e_handlers in self.peng.eventHandlers.items():
            if True or e_type in eh:
                to_del = []
                for e_handler in e_handlers:
                    # Weird workaround due to implementation details of WeakMethod
                    if isinstance(e_handler,weakref.ref):
                        if super(weakref.WeakMethod,e_handler).__call__() is self:
                            to_del.append(e_handler)
                    elif e_handler is self:
                        to_del.append(e_handler)
                for d in to_del:
                    try:
                        #print("Deleting handler %s of type %s"%(d,e_type))
                        del e_handlers[e_handlers.index(d)]
                    except Exception:
                        #print("Could not delete handler %s, memory leak may occur"%d)
                        import traceback;traceback.print_exc()
    #def __del__(self):
    #    print("del %s"%self.name)
    #    try:
    #        del self.bg
    #    except Exception:
    #        pass

class Widget(BasicWidget):
    """
    Subclass of :py:class:`BasicWidget` adding support for changing the :py:class:`Background`\ .

    If no background is given, an :py:class:`EmptyBackground` will be used instead.
    """
    def __init__(self,name,submenu,window,peng,
                 pos=None,size=None,
                 bg=None,
                 min_size=None,
                ):
        if bg is None:
            bg = EmptyBackground(self)
        self.bg = bg
        self.min_size = min_size
        super(Widget,self).__init__(name,submenu,window,peng,pos,size)
    def setBackground(self,bg):
        """
        Sets the background of the widget.

        This may cause the background to be initialized.
        """
        self.bg = bg
        self.redraw()

    def getMinSize(self):
        c = self.getContentSize()
        m = self.min_size if self.min_size is not None else [0,0]
        return [max(c[0],m[0]),max(c[1],m[1])]
    def getContentSize(self):
        return [0,0]

    def on_redraw(self):
        """
        Draws the background and the widget itself.

        Subclasses should use ``super()`` to call this method, or rendering may glitch out.
        """
        if self.bg is not None:
            if not self.bg.initialized:
                self.bg.init_bg()
                self.bg.initialized=True
            self.bg.redraw_bg()
        super(Widget,self).on_redraw()


#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  model.py
#
#  Copyright 2017 notna <notna@apparat.org>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#
#

__all__ = [
    "grouper","calcSphereCoordinates",
    "v_magnitude","v_normalize",
    "Material", "Bone", "RootBone",
    "Region", "Animation",
    "JSONModelGroup","JSONRegionGroup",
    "Model",
    ]

import time

import math
from math import sqrt,cos,sin

try:
    from itertools import zip_longest
except ImportError:
    pass # probably on readthedocs

try:
    import pyglet
    from pyglet.gl import *
except ImportError:
    pass # probably headless

def grouper(iterable, n, fillvalue=None):
    """
    Allows for iteration over multiple elements of a iterable at once.

    ``iterable`` may be any iterable, its values will be returned. Note that this may be iterated over more than once.

    ``n`` is the size of each group. May be any positive integer.

    ``fillvalue`` is optionally used to fill any groups that do not have enough items, for example if the length of the iterable is not divisible by n.

    Example::

        >>> for i in grouper("foobarbaz",2,fillvalue=" "):
        ...     print(i)
        fo
        ob
        ar
        ba
        z  # Note the extra space after the z
    """
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)

def calcSphereCoordinates(pos,radius,rot):
    """
    Calculates the Cartesian coordinates from spherical coordinates.

    ``pos`` is a simple offset to offset the result with.

    ``radius`` is the radius of the input.

    ``rot`` is a 2-tuple of ``(azimuth,polar)`` angles.

    Angles are given in degrees. Most directions in this game use the same convention.

    The azimuth ranges from 0 to 360 degrees with 0 degrees pointing directly to the x-axis.

    The polar angle ranges from -90 to 90 with -90 degrees pointing straight down and 90 degrees straight up.

    A visualization of the angles required is given in the source code of this function.
    """
    # Input angles should be in degrees, as in the rest of the game
    # E.g. phi=inclination and theta=azimuth

    # phi is yrad
    #      Look from above
    #(Z goes positive towards you)
    #
    #            Y-  Z-
    #            |  /
    #            | / "far"
    #            |/
    #   X- ------+-------> X+
    #           /| yrad |
    #   "near" / |<-----+
    #         /  |  "polar angle"
    #        Z+  Y+

    # theta is xrad
    #      Look from above
    #(Z goes positive towards you)
    #
    #            Y-  Z-
    #            |  /
    #            | / "far"
    #            |/
    #   X- ------+-------> X+
    #           /| xrad |
    #   "near" /<-------+
    #         /  |  "azimuth angle"
    #        Z+  Y+

    # Based on http://stackoverflow.com/questions/39647735/calculation-of-spherical-coordinates
    # https://en.wikipedia.org/wiki/Spherical_coordinate_system
    # http://stackoverflow.com/questions/25404613/converting-spherical-coordinates-to-cartesian?rq=1

    phi,theta = rot
    phi+=90 # very important, took me four days of head-scratching to figure out
    phi,theta = math.radians(phi),math.radians(theta)

    x = pos[0]+radius * math.sin(phi) * math.cos(theta)
    y = pos[1]+radius * math.sin(phi) * math.sin(theta)
    z = pos[2]+radius * math.cos(phi)
    return x,y,z

def v_magnitude(v):
    """
    Simple vector helper function returning the length of a vector.

    ``v`` may be any vector, with any number of dimensions
    """
    return math.sqrt(sum(v[i]*v[i] for i in range(len(v))))

def v_normalize(v):
    """
    Normalizes the given vector.

    The vector given may have any number of dimensions.
    """
    vmag = v_magnitude(v)
    return [ v[i]/vmag  for i in range(len(v)) ]

class Material(object):
    """
    Object that describes a single material of a model.

    This object stores all relevant data and caches. Note that this object is only created once for each model and shared between all rendered instances of it.

    See :py:class:`Model` for more information about the model system.
    """
    def __init__(self,rsrcMgr,name,matdata):
        self.rsrcMgr = rsrcMgr

        self.name = name
        self.matdata = matdata

        self.texname = matdata.get("tex","peng3d:missingtexture")
        self.texcat = matdata.get("texcat","entity")
        if self.texcat not in self.rsrcMgr.categories:
            self.rsrcMgr.addCategory(self.texcat)

    @property
    def target(self):
        """
        Read-only property storing the OpenGL constant representing the target of the texture of this material.

        Commonly :py:const:`GL_TEXTURE_2D` or :py:const:`GL_TEXTURE_3D`\ .

        Used in texture manipulation and activation, e.g. ``glEnable(material.target)``\ .
        """
        return self.rsrcMgr.getTex(self.texname,self.texcat)[0]
    @property
    def id(self):
        """
        Read-only property storing the numerical ID of the texture of this material.

        Used to manipulate the texture behind this material.

        Commonly used in binding the texture: ``glBindTexture(material.target,material.id)``\ .
        """
        return self.rsrcMgr.getTex(self.texname,self.texcat)[1]
    @property
    def tex_coords(self):
        """
        Read-only property storing the texture coordinates to use when drawing with this texture.

        Should not be used directly, see :py:meth:`transformTexCoords()`\ .

        Enables substitution of pyglet :py:class:`pyglet.graphics.Texture` objects with Materials in many places, e.g. in :py:class:`pyglet.graphics.TextureGroup`\ .
        """
        return self.rsrcMgr.getTex(self.texname,self.texcat)[2]

    @property
    def texdata(self):
        """
        Read-only property equivalent to a 3-tuple containing :py:attr:`target`\ , :py:attr:`id` and :py:attr:`tex_coords`\ .

        Should be faster than getting each value directly. Useful if all of these values are needed.
        """
        return self.rsrcMgr.getTex(self,texname,self.texcat)

    def transformTexCoords(self,data,texcoords,dims=2):
        """
        Transforms the given texture coordinates using the internal texture coordinates.

        Currently, the dimensionality of the input texture coordinates must always be 2 and the output is 3-dimensional with the last coordinate always being zero.

        The given texture coordinates are fitted to the internal texture coordinates. Note that values higher than 1 or lower than 0 may result in unexpected visual glitches.

        The length of the given texture coordinates should be divisible by the dimensionality.
        """
        assert dims==2 # TODO

        out = []

        origcoords = self.tex_coords
        min_u,min_v = origcoords[0],origcoords[1]
        max_u,max_v = origcoords[6],origcoords[7]

        diff_u,diff_v = max_u-min_u, max_v-min_v

        itexcoords = iter(texcoords)
        for u,v in zip(itexcoords,itexcoords): # Based on http://stackoverflow.com/a/5389547/3490549
            out_u = min_u+(diff_u*u)
            out_v = min_v+(diff_v*v)
            out.extend((out_u,out_v,0))

        return out # output dimensionality is 3, e.g. t3f

class Bone(object):
    """
    Object that represents a single bone of a model.

    This object stores all relevant data and caches. Note that this object is only created once for each model and shared between all rendered instances of it.

    Actual bone rotation and length is stored per entity and not per model allowing for different bone rotations for multiple entities using the same model.

    See :py:class:`Model` for more information about the model system.
    """
    def __init__(self,rsrcMgr,name,bonedata):
        self.rsrcMgr = rsrcMgr

        self.name = name
        self.bonedata = bonedata

        self.regions = {}
        self.child_bones = {}

        self.parent = None
        self.start_rot = bonedata.get("start_rot",[0,0])
        self.blength = bonedata.get("length",1.0)

    def ensureBones(self,data):
        """
        Helper method ensuring per-entity bone data has been properly initialized.

        Should be called at the start of every method accessing per-entity data.

        ``data`` is the entity to check in dictionary form.
        """
        if "_bones" not in data:
            data["_bones"]={}
        if self.name not in data["_bones"]:
            data["_bones"][self.name]={"rot":self.start_rot[:],"length":self.blength}

    def setRot(self,data,rot):
        """
        Sets the rotation of this bone on the given entity.

        ``data`` is the entity to modify in dictionary form.

        ``rot`` is the rotation of the bone in the format used in :py:func:`calcSphereCoordinates()`\ .
        """
        self.ensureBones(data)
        rot = rot[0]%360,max(-90, min(90, rot[1]))
        data["_bones"][self.name]["rot"]=rot
    def getRot(self,data):
        """
        Returns the rotation of this bone in the given entity.

        ``data`` is the entity to query in dictionary form.
        """
        self.ensureBones(data)
        return data["_bones"][self.name]["rot"]

    def setLength(self,data,blength):
        """
        Sets the length of this bone on the given entity.

        ``data`` is the entity to modify in dictionary form.

        ``blength`` is the new length of the bone.
        """
        self.ensureBones(data)
        data["_bones"][self.name]["length"]=blength
    def getLength(self,data):
        """
        Returns the length of this bone in the given entity.

        ``data`` is the entity to query in dictionary form.
        """
        self.ensureBones(data)
        return data["_bones"][self.name]["length"]

    # No properties to access rot/length directly as they are entity-dependent

    def setParent(self,parent):
        """
        Sets the parent of this bone for all entities.

        Note that this method must be called before many other methods to ensure internal state has been initialized.

        This method also registers this bone as a child of its parent.
        """
        self.parent = parent
        self.parent.child_bones[self.name]=self

    def setRotate(self,data):
        """
        Sets the OpenGL state required for proper drawing of the model.

        Mostly rotates and translates the camera.

        It is important to call :py:meth:`unsetRotate()` after calling this method to properly unset state and avoid OpenGL errors.
        """
        self.parent.setRotate(data)
        glPushMatrix()
        x,y = self.getRot(data)
        pivot = self.getPivotPoint(data)
        px,py,pz = pivot

        #ppx,ppy,ppz = self.parent.getPivotPoint(data)
        #rot_vec = px-ppx,py-ppy,pz-ppz
        #print([px,py,pz],[ppx,ppy,ppz],rot_vec,[x,y],self.getLength(data))
        #norm = v_normalize(rot_vec,self.name) if v_magnitude(rot_vec)==0 else [0,1,0] # prevents a zero divison error
        normx = [0,1,0] # currently fixed to the up vector, should be changed in the future for proper rotation that is not along the y or z axis
        normy = [0,0,1]

        #offset = -px,-py,-pz

        glTranslatef(px,py,pz)#-offset[0],-offset[1],-offset[2])
        glRotatef(x-self.start_rot[0], normx[0],normx[1],normx[2])
        glRotatef(y-self.start_rot[1], normy[0],normy[1],normy[2])
        glTranslatef(-px,-py,-pz)#offset[0],offset[1],offset[2])
    def unsetRotate(self,data):
        """
        Unsets the OpenGL state that was set before calling :py:meth:`setRotate()`\ .

        Note that this method may cause various OpenGL errors if called without :py:meth:`setRotate()` having been called.
        """
        glPopMatrix()
        self.parent.unsetRotate(data)

    def getPivotPoint(self,data):
        """
        Returns the point this bone pivots around on the given entity.

        This method works recursively by calling its parent and then adding its own offset.

        The resulting coordinate is relative to the entity, not the world.
        """
        ppos = self.parent.getPivotPoint(data)
        rot = self.parent.getRot(data)
        length = self.parent.getLength(data)
        out = calcSphereCoordinates(ppos,length,rot)
        return out

    def transformVertices(self,data,vertices,dims=3):
        """
        Currently unused method that transforms the given vertices according to the rotation of the bone.

        Currently just returns the vertices unmodified, will be implemented in the future.
        """
        assert dims==3

        # vertices are not transformed directly, rather they are rotated dynamically via glRotate

        return vertices

    def addRegion(self,region):
        """
        Register a vertex Region as a dependent of this bone.

        ``region`` must be an instance of :py:class:`Region`\ .
        """
        self.regions[region.name]=region

class RootBone(Bone):
    """
    Special bone that represents the root of a entity.

    This bone is immutable and cannot be rotated or otherwise modified.
    """
    def getPivotPoint(self,data):
        return [0,0,0]
    getPivotPoint.__noautodoc__ = True
    def getLength(self,data):
        return 0
    getLength.__noautodoc__ = True
    def setRotate(self,data):
        pass # no rotation as the root bone is always pointed straight up
    setRotate.__noautodoc__ = True
    def unsetRotate(self,data):
        pass # no unset as there is no set
    unsetRotate.__noautodoc__ = True

class Region(object):
    """
    Object that represents a vertex region of a model.

    A vertex region is associated with a specific bone of the same model it is associated with.
    It has a list of vertices and optionally texture coordinates. The texture coordinates are transformed using the material it is associated with.

    Most regions will use quads as their primitive type, but it is also possible to use triangles, lines and points.

    To use quads as the geometry type, specify either ``quads``\ , ``quad`` or ``GL_QUADS`` as its ``geometry_type``\ .

    To use triangles as the geometry type, specify either ``tris``\ , ``triangles``\ , ``triangle`` or ``GL_TRIANGLES`` as its ``geometry_type``\ .

    To use lines as the geometry type, specify either ``lines``\ , ``line`` or ``GL_LINES`` as its ``geometry_type``\ .

    To use points as the geometry type, specify either ``points``\ , ``point``\ , ``dots``\, ``dot`` or ``GL_POINTS`` as its ``geometry_type``\ .

    Note that the number of vertices must be divisible by the number of vertices required per primitive, e.g. 4 for quads, 3 for triangles, 2 for lines and 1 for points.

    Additionally, the number of vertices and texture coordinate pairs must also match.

    If any of these conditions are not fulfilled, a :py:exc:`ValueError` will be raised.
    """
    def __init__(self,rsrcMgr,name,regdata):
        self.rsrcMgr = rsrcMgr

        self.name = name
        self.regdata = regdata

        self.material = None
        self.bone = None

        self.dims = 3
        self.tex_dims = 2

        gtype = regdata.get("geometry_type","quads")
        if gtype in ["quads","quad","GL_QUADS"]:
            self.geometry_type = GL_QUADS
            # points per primitive
            ppp = 4
        elif gtype in ["tris","triangles","triangle","GL_TRIANGLES"]:
            self.geometry_type = GL_TRIANGLES
            ppp = 3
        elif gtype in ["lines","line","GL_LINES"]:
            self.geometry_type = GL_LINES
            ppp = 2
        elif gtype in ["points","point","dots","dot","GL_POINTS"]:
            self.geometry_type = GL_POINTS
            ppp = 1
        else:
            raise ValueError("Invalid Geometry type %s"%gtype)

        self.vertices = regdata.get("vertices",[])
        if len(self.vertices)%self.dims!=0:
            raise ValueError("Vertices must be in x,y,z groups")
        elif (len(self.vertices)/self.dims)%ppp!=0:
            raise ValueError("Invalid amount of x,y,z groups, must be integer-divisible by %s"%ppp)

        if "tex_coords" in regdata:
            self.enable_tex = True
            self.tex_coords = regdata.get("tex_coords",[])
            if len(self.tex_coords)/self.tex_dims!=len(self.vertices)/self.dims:
                raise ValueError("Non-Matching amount of vertices and tex coords")
            elif len(self.tex_coords)%self.tex_dims!=0:
                raise ValueError("Tex coords must be in u,v pairs")
            elif (len(self.tex_coords)/self.tex_dims)%ppp!=0:
                raise ValueError("Invalid amount of u,v pairs, must be integer-divisible by %s"%ppp)
        else:
            self.enable_tex = False
            self.tex_coords = [0]*(self.tex_dims*(len(self.vertices)/self.dims))

    def getVertices(self,data):
        """
        Returns the vertices of this region already transformed and ready-to-use.

        Internally uses :py:meth:`Bone.transformVertices()`\ .
        """
        return self.bone.transformVertices(data,self.vertices,self.dims)
    def getGeometryType(self,data):
        """
        Returns the OpenGL constant representing the type of primitives used by this region.

        May be one of :py:data:`GL_QUADS`\ , :py:data:`GL_TRIANGLES`\ , :py:data:`GL_LINES` or :py:data:`GL_POINTS`\ .
        """
        return self.geometry_type
    def getTexCoords(self,data):
        """
        Returns the texture coordinates, if any, to accompany the vertices of this region already transformed.

        Note that it is recommended to check the :py:attr:`enable_tex` flag first.

        Internally uses :py:meth:`Material.transformTexCoords()`\ .
        """
        return self.material.transformTexCoords(data,self.tex_coords,self.tex_dims)
    def getTexInfo(self,data):
        """
        Returns informations about the texture of this region.

        Internally uses :py:attr:`Material.texdata`\ , exact specification available there.
        """
        return self.material.texdata

class Animation(object):
    """
    Object that represents an animation of a model.

    Animations can be either static or animated using keyframes.

    See :py:class:`Model` for more information.
    """
    def __init__(self,rsrcMgr,name,anidata):
        self.rsrcMgr = rsrcMgr

        self.name = name
        self.anidata = anidata

        self.bones = {}

        self.atype = anidata.get("type","bone" if "keyframes" in anidata else "static")

        self.default_jt = anidata.get("default_jumptype","animate")

        self.entity_defaults = {}
        self.entity_template = {}

        if self.atype == "static":
            self.start_frame = {"bones":anidata.get("bones")}
        elif self.atype == "keyframes":
            self.kps = anidata.get("keyframespersecond",60)
            self.anilength = anidata.get("length",self.kps)

            self.lframes_per_bone = {} # for length-setting keyframes
            self.rframes_per_bone = {} # for rotating keyframes

            minframe = None
            for frame,keyframe in anidata.get("keyframes",{}).items():
                frame = int(frame)
                if minframe is None or frame<minframe:
                    minframe = frame

                for bname,bone in keyframe.get("bones",{}).items():
                    if "rot" in bone:
                        if bname not in self.rframes_per_bone:
                            self.rframes_per_bone[bname]={}
                        self.rframes_per_bone[bname][frame]=bone["rot"]
                    if "length" in bone:
                        if bname not in self.lframes_per_bone:
                            self.lframes_per_bone[bname]={}
                        self.lframes_per_bone[bname][frame]=bone["length"]

            self.start_frame = anidata["keyframes"][str(minframe)] if minframe is not None else {"bones":{}}
            self.start_frame["frame"] = minframe if minframe is not None else 0
        else:
            raise ValueError("Invalid animation type %s for animation %s"%(self.atype,name))

    def setBones(self,bones):
        """
        Sets the internal dictionary of bones in the parent model.

        Must be a dictionary, else errors may appear later on.
        """
        self.bones = bones

    def startAnimation(self,data,jumptype):
        """
        Callback that is called to initialize this animation on a specific actor.

        Internally sets the ``_anidata`` key of the given dict ``data``\ .

        ``jumptype`` is either ``jump`` or ``animate`` to define how to switch to this animation.
        """
        data["_anidata"]={}
        adata = data["_anidata"]

        adata["keyframe"]=0
        adata["last_tick"]=time.time()
        adata["jumptype"]=jumptype
        adata["phase"]="transition"

    def tickEntity(self,data):
        """
        Callback that should be called regularly to update the animation.

        It is recommended to call this method about 60 times a second for smooth animations. Irregular calling of this method will be automatically adjusted.

        This method sets all the bones in the given actor to the next state of the animation.

        Note that :py:meth:`startAnimation()` must have been called before calling this method.
        """
        adata = data["_anidata"]

        if adata.get("anitype",self.name)!=self.name:
            return # incorrectly called

        dt = time.time()-adata.get("last_tick",time.time())
        adata["last_tick"]=time.time()

        if adata.get("phase","transition")=="transition":
            # If transitioning to this animation
            if adata.get("jumptype",self.default_jt)=="jump":
                # Jumping to the animation, e.g. set all bones to the start frame
                for bone,dat in self.start_frame.get("bones",{}).items():
                    if "rot" in dat:
                        self.bones[bone].setRot(data,dat["rot"])
                    if "length" in dat:
                        self.bones[bone].setLength(data,dat["length"])
                adata["phase"]="animation"
                if self.atype=="keyframes":
                    adata["from_frame"]=self.start_frame.get("frame",0)
            elif adata.get("jumptype",self.default_jt)=="animate":
                raise NotImplementedError("Animation update phase transition type animate not yet implemented")

                # Not yet working
                if "transition_frame" not in adata:
                    adata["transition_frame"] = 0
                tlen = self.anidata.get("transition_length",self.anidata.get("length",60)/3)
                tspeed = self.anidata.get("transition_speed",self.anidata.get("keyframespersecond",60))

                framediff=int(dt/(1./tspeed)) # Number of frames that have passed since the last update
                overhang = dt-(framediff*(1./tspeed)) # time that has passed but is not enough for a full frame
                adata["last_tick"]-=overhang # causes the next frame to include the overhang from this frame
                if framediff == 0:
                    return # optimization that saves time

                frame1 = adata["transition_frame"]
                adata["transition_frame"]+=framediff
                if adata["transition_frame"]>tlen:
                    adata["phase"]="animation"
                    if self.atype=="keyframes":
                        adata["from_frame"]=self.start_frame.get("frame",0)
                    return
                frame2 = adata["transition_frame"]

                if "frombones" not in adata:
                    frombones = {}
                    for bname,bone in self.bones.items():
                        frombones[bname]={"rot":bone.getRot(data),"length":bone.getLength(data)}
                    adata["frombones"] = frombones
                if "tobones" not in adata:
                    tobones = {}
                    for bname,bone in self.start_frame["bones"].items():
                        tobones[bname]=bone
                    adata["tobones"] = tobones

                frombones = adata["frombones"]
                tobones = adata["tobones"]

                from_frame = 0
                to_frame = tlen

                for bname,bone in self.bones.items():
                    # rot
                    if bname not in frombones or bname not in tobones:
                        continue

                    from_rot = frombones[bname]["rot"]
                    to_rot = tobones[bname]["rot"]

                    from_x,from_y=from_rot
                    to_x,to_y=to_rot

                    delta_per_frame_x = (to_x-from_x)/(to_frame-from_frame)
                    delta_per_frame_y = (to_y-from_y)/(to_frame-from_frame)

                    delta_x = delta_per_frame_x*(frame2-frame1)
                    delta_y = delta_per_frame_y*(frame2-frame1)

                    rot = bone.getRot(data)
                    new_rot = [rot[0]+delta_x,rot[1]+delta_y]

                    bone.setRot(data,new_rot)

                    # length

                    # Not yet implemented

        elif adata["phase"]=="animation":
            # If animating
            if self.atype == "static":
                pass # Should not need any updates as the transition will set the bones
            elif self.atype == "keyframes":
                framediff=int(dt/(1./self.kps)) # Number of frames that have passed since the last update
                overhang = dt-(framediff*(1./self.kps)) # time that has passed but is not enough for a full frame
                adata["last_tick"]-=overhang # causes the next frame to include the overhang from this frame
                if framediff == 0:
                    return # optimization that saves time

                frame1 = adata["keyframe"]
                adata["keyframe"]+=framediff
                if adata["keyframe"]>self.anilength:
                    repeat = True
                    adata["keyframe"]%=self.anilength
                else:
                    repeat = False
                frame2 = adata["keyframe"]

                if repeat:
                    frame1 = frame2
                    frame2+=1

                for bname,bone in self.bones.items():
                    # Rot
                    if bname in self.rframes_per_bone:

                        from_frame = None#adata["from_frame"]
                        to_frame = None
                        for framenum,rot in self.rframes_per_bone[bname].items():
                            if (from_frame is None or framenum>from_frame) and framenum<=frame1:
                                # from_frame is the largest frame number that is before the starting point
                                from_frame = framenum
                            if (to_frame is None or framenum<to_frame) and framenum>=frame2:
                                # to_frame is the smallest frame number that is after the end point
                                to_frame = framenum
                        if from_frame is None or to_frame is None:
                            raise ValueError("Invalid frames for bone %s in animation %s"%(bname,self.name))
                        if from_frame==to_frame or repeat:
                            if self.anidata.get("repeat","jump")=="jump":
                                for b,dat in self.start_frame.get("bones",{}).items():
                                    if "rot" in dat:
                                        self.bones[b].setRot(data,dat["rot"])
                                    if "length" in dat:
                                        self.bones[b].setLength(data,dat["length"])
                            elif self.anidata.get("repeat","jump")=="animate":
                                from_frame = adata["to_frame"]
                        adata["from_frame"]=from_frame
                        adata["to_frame"]=to_frame

                        from_rot = self.rframes_per_bone[bname][from_frame]
                        to_rot = self.rframes_per_bone[bname][to_frame]

                        from_x,from_y=from_rot
                        to_x,to_y=to_rot

                        if self.anidata.get("interpolation","linear") == "linear":
                            delta_per_frame_x = (to_x-from_x)/(to_frame-from_frame)
                            delta_per_frame_y = (to_y-from_y)/(to_frame-from_frame)

                            delta_x = delta_per_frame_x*(frame2-frame1)
                            delta_y = delta_per_frame_y*(frame2-frame1)

                            rot = bone.getRot(data)
                            new_rot = [rot[0]+delta_x,rot[1]+delta_y]
                        elif self.anidata["interpolation"] == "jump":
                            new_rot = to_x,to_y
                        else:
                            raise ValueError("Invalid interpolation method '%s' for animation %s"%(self.anidata["interpolation"],self.name))

                        bone.setRot(data,new_rot)

                    # Length
                    if bname in self.lframes_per_bone:
                        # Not yet implemented
                        pass

            else:
                # Should not be possible, but still
                raise ValueError("Invalid animation type %s for animation %s during tick"%(self.atype,name))

class JSONModelGroup(pyglet.graphics.Group):
    """
    Pyglet group that sets the state required by a specific actor.

    This group should always be set during any draw operations for the assigned actor.
    This can either be done by setting it as the group of a vertex list,
    the parent group of a group of a vertex list
    or manually calling :py:meth:`set_state()` and :py:meth:`unset_state()`\ .
    """
    def __init__(self,model,data,obj,parent=None):
        super(JSONModelGroup,self).__init__(parent)

        self.model = model
        self.data = data
        self.obj = obj

    def set_state(self):
        """
        Sets the state required for this actor.

        Currently translates the matrix to the position of the actor.
        """
        x,y,z = self.obj.pos
        glTranslatef(x,y,z)

    def unset_state(self):
        """
        Resets the state required for this actor to the default state.

        Currently resets the matrix to its previous translation.
        """
        x,y,z = self.obj.pos
        glTranslatef(-x,-y,-z)

    def __hash__(self):
        # Internal method to allow unification of multiple groups of the same actor
        return hash((self.obj,))

    def __eq__(self,other):
        return (self.__class__ is other.__class__ and
                self.obj.pos == other.obj.pos and
                self.parent == other.parent
               )

class JSONRegionGroup(pyglet.graphics.Group):
    """
    Pyglet group that manages the state required by a specific vertex region of an actor.

    This group and the associated :py:class:`JSONModelGroup` should always be set during any draw operation for the assigned region.

    See :py:class:`JSONModelGroup` for more information about how to do this.
    """
    def __init__(self,model,data,region,parent=None):
        super(JSONRegionGroup,self).__init__(parent)

        self.model = model
        self.data = data
        self.region = region

    def set_state(self):
        """
        Sets the state required for this vertex region.

        Currently binds and enables the texture of the material of the region.
        """
        glEnable(self.region.material.target)
        glBindTexture(self.region.material.target, self.region.material.id)
        self.region.bone.setRotate(self.data)

    def unset_state(self):
        """
        Resets the state required for this actor to the default state.

        Currently only disables the target of the texture of the material, it may still be bound.
        """
        glDisable(self.region.material.target)
        self.region.bone.unsetRotate(self.data)

    def __hash__(self):
        return hash((self.region.material.target, self.region.material.id, self.parent))

    def __eq__(self, other):
        return (self.__class__ is other.__class__ and
            self.region.material.target == other.region.material.target and
            self.region.material.id == other.region.material.id and
            self.region.bone is other.region.bone and
            self.region.bone.getRot(self.data) == other.region.bone.getRot(other.data) and
            self.region.bone.getLength(self.data) == other.region.bone.getLength(other.data) and # TODO: include parent bones
            self.parent == other.parent
            )

class Model(object):
    """
    Object that represents the model of an actor.

    Note that this object is not bound to an actor but rather to a collection of materials, bones, vertex regions and animations.

    A single instance of this class may be used by multiple actors at the same time. See :py:meth:`Actor.setModel()` for more information.

    A test model is available at ``assets/peng3d/model/test.json`` and a demo program using it under ``test_model.py``\ .

    .. todo::

       Document the format of .json model files.
    """
    def __init__(self,peng,rsrcMgr,name):
        self.peng = peng
        self.rsrcMgr = rsrcMgr
        self.name = name

        self.modeldata = self.rsrcMgr.getModelData(name)

    def ensureModelData(self,obj):
        """
        Ensures that the given ``obj`` has been initialized to be used with this model.

        If the object is found to not be initialized, it will be initialized.
        """
        if not hasattr(obj,"_modeldata"):
            self.create(obj,cache=True)
        if "_modelcache" not in obj._modeldata:
            # Assume all initialization is missing, simply reinitialize
            self.create(obj,cache=True)

    def create(self,obj,cache=False):
        """
        Initializes per-actor data on the given object for this model.

        If ``cache`` is set to True, the entity will not be redrawn after initialization.

        Note that this method may set several attributes on the given object, most of them starting with underscores.

        During initialization of vertex regions, several vertex lists will be created.
        If the given object has an attribute called ``batch3d`` it will be used, else it will be created.

        If the batch already existed, the :py:meth:`draw()` method will do nothing, else it will draw the batch.

        Memory leaks may occur if this is called more than once on the same object without calling :py:meth:`cleanup()` first.
        """
        obj._modeldata = {}

        data = obj._modeldata

        data["_model"]=self
        data["_modelcache"] = {}
        moddata = data["_modelcache"]

        # Model group, for pos of entity
        moddata["group"] = JSONModelGroup(self,data,obj)
        modgroup = moddata["group"]

        if not hasattr(obj,"batch3d"):
            obj.batch3d = pyglet.graphics.Batch()
            data["_manual_render"]=True

        # Vlists/Regions
        moddata["vlists"] = {}
        for name,region in self.modeldata["regions"].items():
            v = region.getVertices(data)
            vlistlen = int(len(v)/region.dims)

            if region.enable_tex:
                vlist = obj.batch3d.add(vlistlen,region.getGeometryType(data),JSONRegionGroup(self,data,region,modgroup),
                        "v3f/static",
                        "t3f/static",
                        )
            else:
                vlist = obj.batch3d.add(vlistlen,region.getGeometryType(data),modgroup,
                        "v3f/static",
                        )

            moddata["vlists"][name]=vlist

        self.setAnimation(obj,self.modeldata["default_animation"].name,transition="jump")

        self.data = data

        if not cache:
            self.redraw(obj)

    def cleanup(self,obj):
        """
        Cleans up any left over data structures, including vertex lists that reside in GPU memory.

        Behaviour is undefined if it is attempted to use this model with the same object without calling :py:meth:`create()` first.

        It is very important to call this method manually during deletion as this will delete references to data objects stored in global variables of third-party modules.
        """
        if not hasattr(obj,"_modeldata"):
            return # already not initialized
        data = obj._modeldata

        if "_modelcache" in data:
            moddata = data["_modelcache"]

            if "vlists" in moddata:
                for vlist in list(moddata["vlists"].keys()):
                    del moddata["vlists"][vlist]
                del moddata["vlists"]

        if "_bones" in data:
            for bone in list(data["_bones"].keys()):
                del data["_bones"][bone]
            del data["_bones"]

        if "_anidata" in data:
            adata = data["_anidata"]
            if "_schedfunc" in adata:
                pyglet.clock.unschedule(adata["_schedfunc"])

        if data.get("_manual_render",False):
            del obj.batch3d

        # Removes open vertex lists and other caches

        if "_modelcache" not in data:
            return
        moddata = data["_modelcache"]

        if "vlist" in moddata:
            moddata["vlist"].delete() # Free up the graphics memory
        if "group" in moddata:
            del moddata["group"]

        del data["_modelcache"], moddata

    def redraw(self,obj):
        """
        Redraws the model of the given object.

        Note that currently this method probably won't change any data since all movement and animation is done through pyglet groups.
        """
        self.ensureModelData(obj)
        data = obj._modeldata

        vlists = data["_modelcache"]["vlists"]

        for name,region in self.modeldata["regions"].items():
            vlists[name].vertices = region.getVertices(data)
            if region.enable_tex:
                vlists[name].tex_coords = region.getTexCoords(data)

    def draw(self,obj):
        """
        Actually draws the model of the given object to the render target.

        Note that if the batch used for this object already existed, drawing will be skipped as the batch should be drawn by the owner of it.
        """
        self.ensureModelData(obj)

        data = obj._modeldata
        if data.get("_manual_render",False):
            obj.batch3d.draw()

    def remove(self,obj):
        """
        Called if the actor is removed from the world.

        Can be extended for custom features, currently calls :py:meth:`cleanup()`\ .
        """
        # Can be extended, but the base class should always be called
        self.cleanup(obj)

    def setAnimation(self,obj,animation,transition=None,force=False):
        """
        Sets the animation to be used by the object.

        See :py:meth:`Actor.setAnimation()` for more information.
        """
        self.ensureModelData(obj)
        data = obj._modeldata

        # Validity check
        if animation not in self.modeldata["animations"]:
            raise ValueError("There is no animation of name '%s' for model '%s'"%(animation,self.modelname))

        if data.get("_anidata",{}).get("anitype",None)==animation and not force:
            return # animation is already running

        # Cache the obj to improve readability
        anim = self.modeldata["animations"][animation]

        # Set to default if not set
        if transition is None:
            transition = anim.default_jt

        # Notify the animation to allow it to initialize itself
        anim.startAnimation(data,transition)

        # initialize animation data
        if "_anidata" not in data:
            data["_anidata"]={}
        adata = data["_anidata"]
        adata["anitype"]=animation

        if "_schedfunc" in adata:
            # unschedule the old animation, if any
            # prevents clashing and crashes
            pyglet.clock.unschedule(adata["_schedfunc"])

        # Schedule the animation function
        def schedfunc(*args):
            # This function is defined locally to create a closure
            # The closure stores the local variables, e.g. anim and data even after the parent function has finished
            # Note that this may also prevent the garbage collection of any objects defined in the parent scope
            anim.tickEntity(data)

        # register the function to pyglet
        pyglet.clock.schedule_interval(schedfunc,1./(anim.kps if anim.atype=="keyframes" else 60))
        # save it for later for de-initialization
        adata["_schedfunc"] = schedfunc


#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  __init__.py
#
#  Copyright 2016 notna <notna@apparat.org>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#
#

__all__ = ["Actor","RotatableActor","RotateableActor","Controller"]

import uuid as uuidm

class Controller(object):
    """
    Base class for all controllers.

    Controllers define behavior of Actors and can be used to control them via e.g. the keyboard or an AI.

    Every controller is bound to its actor and can be enabled and disabled individually.
    You may also deactivate all controllers of an Actor by setting the ``enabled`` key of :py:attr:`Actor.controlleroptions` to False.
    """
    def __init__(self,actor):
        self.world = actor.world
        self.peng = actor.world.peng
        self.actor = actor
        self._enabled = True
        self.registerEventHandlers()
    def registerEventHandlers(self):
        """
        Method to be overridden by subclasses for registering event handlers.

        Automatically called upon object creation.
        """
        pass

    @property
    def enabled(self):
        """
        Property allowing to get and set if this controller should be active.

        When getting this property, the result of ANDing the internal flag and the actor flag is returned.

        When setting, only the local internal flag is set, allowing other controllers to still work.

        :raises AssertionError: when the supplied value is not of type bool
        """
        return self._enabled and self.actor.controlleroptions["enabled"]
    @enabled.setter
    def enabled(self,value):
        assert isinstance(value,bool)
        self._enabled = value

class Actor(object):
    """
    Actor object, base class for all other Actors in the world.

    An actor represents an object in the world, for example the player, an animal, enemy or dropped item.

    Everything that is not part of the terrain should be an actor.

    The default actor does not do anything, you should look at the subclasses for more information.
    """
    def __init__(self,peng,world,uuid=None,pos=[0,0,0]):
        self.peng = peng
        self.world = world
        self.uuid = uuid if uuid is not None else uuidm.uuid4()
        self.controllers = []
        self.controlleroptions = {
            "enabled":True
            }

        self._pos = pos

        self.model = None

    def addController(self,controller):
        """
        Adds a controller to the actor.

        A controller can control its actor and can act as a bridge between actor and user inputs.

        Controllers may be added anytime during the lifetime of an actor.
        """
        self.controllers.append(controller)

    def setModel(self,model):
        """
        Sets the model this actor should use when drawing.

        This method also automatically initializes the new model and removes the old, if any.
        """
        if self.model is not None:
            self.model.cleanup(self)
        self.model = model
        model.create(self)

    def setAnimation(self,animation,transition=None,force=False):
        """
        Sets the animation the model of this actor should show.

        ``animation`` is the name of the animation to switch to.

        ``transition`` can be used to override the transition between the animations.

        ``force`` can be used to force reset the animation even if it is already running.

        If there is no model set for this actor, a :py:exc:`RuntimeError` will be raised.
        """
        if self.model is None:
            raise RuntimeError("Can only set animation if a model is set")
        self.model.setAnimation(self,animation,transition,force)

    def render(self,view=None):
        """
        Called by :py:meth:`World.render3d()` to render this actor.

        By default, this method calls the draw method of its model, if any.

        For custom render behavior, it is recommended to extend this method or modify the model.
        """
        if self.model is not None:
            self.model.draw(self)

    # Event handlers
    def on_move(self,old):
        """
        Fake event handler called if the location of this actor changes.

        This handler is called after the location has changed.

        :param list old: The previous position
        """
        pass

    # Properties
    @property
    def pos(self):
        """
        Property allowing access to the position of this actor.

        This actor is read-write but calls :py:meth:`on_move()` if it is set.
        """
        return self._pos
    @pos.setter
    def pos(self,value):
        old = self._pos
        self._pos = value
        self.on_move(old)

class RotatableActor(Actor):
    """
    Actor that can also be rotated.

    This subclass adds a rotational value to the actor and a method to move the actor along the current rotation.
    """
    def __init__(self,peng,world,uuid=None,pos=[0,0,0],rot=[0,0]):
        super(RotatableActor,self).__init__(peng,world,uuid,pos)
        self._rot = rot
    def move(self,dist):
        """
        Moves the actor using standard trigonometry along the current rotational vector.

        :param float dist: Distance to move

        .. todo::

           Test this method, also with negative distances
        """
        x, y = self._rot
        y_angle = math.radians(y)
        x_angle = math.radians(x)
        m = math.cos(y_angle)
        dy = math.sin(y_angle)
        dy = 0.0
        dx = math.cos(x_angle)
        dz = math.sin(x_angle)
        dx,dy,dz = dx*dist,dy*dist,dz*dist
        x,y,z = self._pos
        self.pos = x+dx,y+dy,z+dz
        return dx,dy,dz
    move.__noautodoc__ = True

    # Event handlers
    def on_rotate(self,old):
        """
        Fake event handler called if the rotation of this actor changes.

        This handler is called after the rotation has been made.

        :param tuple old: Old rotation before rotating
        """
        pass

    # Properties
    @property
    def rot(self):
        """
        Property for accessing the rotation of this actor.

        Rotation is a tuple of ``(x,y)`` where y is clamped to -90 and 90.
        x rolls over at 360, resulting in a seamless experience for players.

        This property may also be written to, this calls :py:meth:`on_rotate()`\ .
        """
        return self._rot
    @rot.setter
    def rot(self,value):
        old = self._rot
        x,y = value
        y = max(-90, min(90, y))
        x %= 360
        self._rot = x,y
        self.on_rotate(old)

RotateableActor = RotatableActor # for people that spell it right


import os

from .main import toxml

class XunitDestination(object):
    """Manages a repository of xunit files, for writing test reports"""

    def __init__(self, root_dir):
        self.root_dir = root_dir
        self.expected_xunit_files = []

    def write_reports(self, relative_path, suite_name, reports,
                      package_name=None):
        """write the collection of reports to the given path"""

        dest_path = self.reserve_file(relative_path)
        with open(dest_path, 'wb') as outf:
            outf.write(toxml(reports, suite_name, package_name=package_name))
        return dest_path

    def reserve_file(self, relative_path):
        """reserve a XML file for the slice at <relative_path>.xml

        - the relative path will be created for you
        - not writing anything to that file is an error
        """
        if os.path.isabs(relative_path):
            raise ValueError('%s must be a relative path' % relative_path)

        dest_path = os.path.join(self.root_dir, '%s.xml' % relative_path)

        if os.path.exists(dest_path):
            raise ValueError('%r must not already exist' % dest_path)

        if dest_path in self.expected_xunit_files:
            raise ValueError('%r already reserved' % dest_path)

        dest_dir = os.path.dirname(dest_path)
        if not os.path.isdir(dest_dir):
            os.makedirs(dest_dir)

        self.expected_xunit_files.append(dest_path)

        return dest_path

    def check(self):
        expected = self.expected_xunit_files
        if not all(os.path.exists(path) for path in expected):
            raise Exception(
                'result files %r reserved by hook have not been produced' % (
                    set(path for path in expected if not os.path.isfile(path))
            ))


"""convert test events to a xunit XML file"""

from xml.etree import ElementTree as et

from datetime import datetime
from socket import gethostname

class Report(object):
    """represents a test case report"""

    def __init__(self, name, start_ts=None, end_ts=None, src_location=None):
        self.name = name
        self.start_ts = start_ts
        self.end_ts = end_ts
        self.src_location = src_location
        self.failures = []
        self.errors = []

    def __repr__(self):
        return '%r' % dict(
            name=self.name,
            start_ts=self.start_ts,
            end_ts=self.end_ts,
            failed=not self.errors,
            src_location=self.src_location,
        )

    def __hash__(self):
        return repr(self).__hash__()

    def __eq__(self, another):
        return repr(self) == repr(another)

class EventReceiver(object):
    """eventfull interface to collect results from test cases and produce test reports."""

    def __init__(self):
        self.cases = []
        self.current_case = None

    def end_current_case(self, ts):
        self.current_case.end_ts = ts
        self.cases.append(self.current_case)

    def begin_case(self, test_name, ts, src_location):
        if self.current_case is not None:
            self.error(ts)
            self.end_current_case(ts)

        self.current_case = Report(test_name)
        self.current_case.start_ts = ts
        self.current_case.src_location = src_location

    def end_case(self, test_name, ts):
        if self.current_case is None or self.current_case.name != test_name:
            raise Exception(
                'cannot close case %s (current: %s)' % (test_name, self.current_case)
            )
        self.end_current_case(ts)
        self.current_case = None

    def error(self, reason):
        assert self.current_case is not None
        self.current_case.errors.append(
            'error %s' % (
                reason
            )
        )

    def failure(self, reason, src_location):
        assert self.current_case is not None
        self.current_case.failures.append(
            'test failure %s at %s' % (
                reason, src_location,
            )
        )

    def results(self):
        if self.current_case is not None:
            self.error('test finished unexpectedly')
            self.end_current_case(self.current_case.start_ts)

        return self.cases

def toxml(test_reports, suite_name,
          hostname=gethostname(), package_name="tests"):
    """convert test reports into an xml file"""

    testsuites = et.Element("testsuites")
    testsuite = et.SubElement(testsuites, "testsuite")

    test_count = len(test_reports)
    if test_count < 1:
        raise ValueError('there must be at least one test report')

    assert test_count > 0, 'expecting at least one test'

    error_count = len([r for r in test_reports if r.errors])
    failure_count = len([r for r in test_reports if r.failures])
    ts = test_reports[0].start_ts
    start_timestamp = datetime.fromtimestamp(ts).isoformat()

    total_duration = test_reports[-1].end_ts - test_reports[0].start_ts

    def quote_attribute(value):
        return value if value is not None else "(null)"

    testsuite.attrib = dict(
        id="0",
        errors=str(error_count),
        failures=str(failure_count),
        tests=str(test_count),
        hostname=quote_attribute(hostname),
        timestamp=quote_attribute(start_timestamp),
        time="%f" % total_duration,
        name=quote_attribute(suite_name),
        package=quote_attribute(package_name),
    )

    for r in test_reports:
        test_name = r.name
        test_duration = r.end_ts - r.start_ts
        class_name = r.src_location

        testcase = et.SubElement(testsuite, "testcase")
        testcase.attrib = dict(
            name=test_name,
            classname=quote_attribute(class_name),
            time="%f" % test_duration,
            )
        if r.errors or r.failures:
            if r.failures:
                failure = et.SubElement(testcase, "failure")
                failure.attrib = dict(
                    type="exception",
                    message=quote_attribute('\n'.join(['%s' % e for e in r.failures])),
                )
            else:
                error = et.SubElement(testcase, "error")
                error.attrib = dict(
                    type="exception",
                    message=quote_attribute('\n'.join(['%s' % e for e in r.errors])),
                )

    return et.tostring(testsuites, encoding="utf-8")


#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  window.py
#
#  Copyright 2016 notna <notna@apparat.org>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#
#

__all__ = ["PengWindow"]

import math
import traceback
import weakref
import inspect

import pyglet
from pyglet.gl import *
from pyglet.window import key

from . import config, camera

ICON_SIZES = [16,24,32,48,64,128,256,512]

class PengWindow(pyglet.window.Window):
    """
    Main window class for peng3d and subclass of :py:class:`pyglet.window.Window()`\ .

    This class should not be instantiated directly, use the :py:meth:`Peng.createWindow()` method.
    """

    def __init__(self,peng,*args,**kwargs):
        if peng.cfg["graphics.stencil.enable"]:
            glconfig = pyglet.gl.Config(stencil_size=peng.cfg["graphics.stencil.bits"])
            kwargs["config"]=glconfig
        super(PengWindow,self).__init__(*args,**kwargs)
        self.peng = peng

        self.exclusive = False
        self.started = False
        self.exclusive = False

        self.menus = {}
        self.activeMenu = None

        self.cfg = config.Config({},defaults=peng.cfg)
        self.eventHandlers = {}

        self._setup = False
        def on_key_press(symbol, modifiers):
            if symbol == key.ESCAPE:
                return pyglet.event.EVENT_HANDLED
        self.push_handlers(on_key_press) # to stop the auto-exit on escape

        self.peng.sendEvent("peng3d:window.create",{"peng":self.peng,"window":self})
    def setup(self):
        """
        Sets up the OpenGL state.

        This method should be called once after the config has been created and before the main loop is started.
        You should not need to manually call this method, as it is automatically called by :py:meth:`run()`\ .

        Repeatedly calling this method has no effects.
        """
        if self._setup:
            return
        self._setup = True

        # Ensures that default values are supplied
        #self.cleanConfig()

        # Sets min/max window size, if specified
        if self.cfg["graphics.min_size"] is not None:
            self.set_minimum_size(*self.cfg["graphics.min_size"])
        if self.cfg["graphics.max_size"] is not None:
            self.set_maximum_size(*self.cfg["graphics.max_size"])

        # Sets up basic OpenGL state
        glClearColor(*self.cfg["graphics.clearColor"])

        glEnable(GL_DEPTH_TEST)
        glEnable(GL_BLEND)

        glShadeModel(GL_SMOOTH)

        if self.cfg["graphics.fogSettings"]["enable"]:
            self.setupFog()
        if self.cfg["graphics.lightSettings"]["enable"]:
            self.setupLight()
    def setupFog(self):
        """
        Sets the fog system up.

        The specific options available are documented under :confval:`graphics.fogSettings`\ .
        """
        fogcfg = self.cfg["graphics.fogSettings"]
        if not fogcfg["enable"]:
            return

        glEnable(GL_FOG)

        if fogcfg["color"] is None:
            fogcfg["color"] = self.cfg["graphics.clearColor"]
        # Set the fog color.
        glFogfv(GL_FOG_COLOR, (GLfloat * 4)(*fogcfg["color"]))
        # Set the performance hint.
        glHint(GL_FOG_HINT, GL_DONT_CARE) # TODO: add customization, including headless support
        # Specify the equation used to compute the blending factor.
        glFogi(GL_FOG_MODE, GL_LINEAR)
        # How close and far away fog starts and ends. The closer the start and end,
        # the denser the fog in the fog range.
        glFogf(GL_FOG_START, fogcfg["start"])
        glFogf(GL_FOG_END, fogcfg["end"])

    def setupLight(self):
        """
        Sets the light system up.

        The specific options available are documented under :confval:`graphics.lightSettings`\ .

        Note that this feature is currently not implemented.
        """
        raise NotImplementedError("Currently not implemented")

    def run(self,evloop=None):
        """
        Runs the application in the current thread.

        This method should not be called directly, especially when using multiple windows, use :py:meth:`Peng.run()` instead.

        Note that this method is blocking as rendering needs to happen in the main thread.
        It is thus recommendable to run your game logic in another thread that should be started before calling this method.

        ``evloop`` may optionally be a subclass of :py:class:`pyglet.app.base.EventLoop` to replace the default event loop.
        """
        self.setup()
        if evloop is not None:
            pyglet.app.event_loop = evloop
        pyglet.app.run() # This currently just calls the basic pyglet main loop, maybe implement custom main loop for more control
    """def cleanConfig(self):
        ########## Reset quotation marks if uncommenting
        Sets default values for various config values.

        .. todo::

           Use an defaultdict or similiar instead.
        ########## End quotation marks
        # Various default values are set in this method, this should really be replaced with something more easy to use and robust
        # A possible replacement could be a defaultdict or similiar
        # Update: now replaced by :py:class:`Config()`\ .

        # OpenGL configs
        self.cfg["graphics.clearColor"] = self.cfg.get("graphics.clearColor",(0.,0.,0.,1.))
        self.cfg["graphics.wireframe"] = self.cfg.get("graphics.wireframe",False)
        self.cfg["graphics.fieldofview"] = self.cfg.get("graphics.fieldofview",65.0)
        self.cfg["graphics.nearclip"] = self.cfg.get("graphics.nearclip",0.1)
        self.cfg["graphics.farclip"] = self.cfg.get("graphics.farclip",10000) # It's over 9000!

        # OpenGL - Fog
        self.cfg["graphics.fogSettings"] = self.cfg.get("graphics.fogSettings",CFG_FOG_DEFAULT)
        self.cfg["graphics.fogSettings"]["enable"] = self.cfg["graphics.fogSettings"].get("enable",False)
        if self.cfg["graphics.fogSettings"]["enable"]:
            pass

        # OpenGL - Light
        self.cfg["graphics.lightSettings"] = self.cfg.get("graphics.lightSettings",CFG_LIGHT_DEFAULT)
        self.cfg["graphics.lightSettings"]["enable"] = self.cfg["graphics.lightSettings"].get("enable",False)
        if self.cfg["graphics.lightSettings"]["enable"]:
            pass

        # Other configs

        return
    """
    # Various methods
    def changeMenu(self,menu):
        """
        Changes to the given menu.

        ``menu`` must be a valid menu name that is currently known.

        .. versionchanged:: 1.2a1

           The push/pop handlers have been deprecated in favor of the new :py:meth:`Menu.on_enter() <peng3d.menu.Menu.on_enter>`\ , :py:meth:`Menu.on_exit() <peng3d.menu.Menu.on_exit>`\ , etc. events.
        """
        if menu not in self.menus:
            raise ValueError("Menu %s does not exist!"%menu)
        elif menu == self.activeMenu:
            return # Ignore double menu activation to prevent bugs in menu initializer
        old = self.activeMenu
        self.activeMenu = menu
        if old is not None:
            self.menus[old].on_exit(menu)
            self.menus[old].doAction("exit")
            #self.pop_handlers()
        self.menu.on_enter(old)
        self.menu.doAction("enter")
        self.peng.sendEvent("peng3d:window.menu.change",{"peng":self.peng,"window":self,"old":old,"menu":menu})
        #self.push_handlers(self.menu)
    def addMenu(self,menu):
        """
        Adds a menu to the list of menus.
        """
        # If there is no menu selected currently, this menu will automatically be made active.
        # Add the line above to the docstring if fixed
        self.menus[menu.name]=menu
        self.peng.sendEvent("peng3d:window.menu.add",{"peng":self.peng,"window":self,"menu":menu})
        #if self.activeMenu is None:
        #    self.changeMenu(menu.name)
        # currently disabled because of a bug with adding layers

    def setIcons(self,icons):
        if isinstance(icons,list) or isinstance(icons,tuple):
            # should be a list of resource names
            rlist = icons
        else:
            # We were given a string, need to expand it first
            rlist = []
            for isize in ICON_SIZES:
                rname = icons.format(size=isize)
                if self.peng.rsrcMgr.resourceExists(rname,".png"):
                    # Resource exists, add it to list
                    rlist.append(rname)

        ilist = []
        for rname in rlist:
            ilist.append(pyglet.image.load(self.peng.rsrcMgr.resourceNameToPath(rname,".png")))

        if len(ilist)!=0:
            self.set_icon(*ilist)

    # Event handlers
    def on_draw(self):
        """
        Clears the screen and draws the currently active menu.
        """
        self.clear()
        self.menu.draw()

    def dispatch_event(self,event_type,*args):
        """
        Internal event handling method.

        This method extends the behavior inherited from :py:meth:`pyglet.window.Window.dispatch_event()` by calling the various :py:meth:`handleEvent()` methods.

        By default, :py:meth:`Peng.handleEvent()`\ , :py:meth:`handleEvent()` and :py:meth:`Menu.handleEvent()` are called in this order to handle events.

        Note that some events may not be handled by all handlers during early startup.
        """
        super(PengWindow,self).dispatch_event(event_type,*args)
        try:
            p = self.peng
            m = self.menu
        except AttributeError:
            # To prevent early startup errors
            if hasattr(self,"peng") and self.peng.cfg["debug.events.logerr"]:
                print("Error:")
                traceback.print_exc()
            return
        p.handleEvent(event_type,args,self)
        self.handleEvent(event_type,args)
        m.handleEvent(event_type,args)

    def handleEvent(self,event_type,args,window=None):
        args = list(args)
        #if window is not None:
        #    args.append(window)
        if event_type in self.eventHandlers:
            for whandler in self.eventHandlers[event_type]:
                # This allows for proper collection of deleted handler methods by using weak references
                handler = whandler()
                if handler is None:
                    del self.eventHandlers[event_type][self.eventHandlers[event_type].index(whandler)]
                handler(*args)
    handleEvent.__noautodoc__ = True

    def registerEventHandler(self,event_type,handler):
        if self.peng.cfg["debug.events.register"]:
            print("Registered Event: %s Handler: %s"%(event_type,handler))
        if event_type not in self.eventHandlers:
            self.eventHandlers[event_type]=[]
        # Only a weak reference is kept
        if inspect.ismethod(handler):
            handler = weakref.WeakMethod(handler)
        else:
            handler = weakref.ref(handler)
        self.eventHandlers[event_type].append(handler)

    # Properties/Proxies for various things

    # Proxy for self.menus[self.activeMenu]
    @property
    def menu(self):
        """
        Property for accessing the currently active menu.

        Always equals ``self.menus[self.activeMenu]``\ .

        This property is read-only.
        """
        return self.menus[self.activeMenu]

    # Utility methods

    def toggle_exclusivity(self,override=None):
        """
        Toggles mouse exclusivity via pyglet's :py:meth:`set_exclusive_mouse()` method.

        If ``override`` is given, it will be used instead.

        You may also read the current exclusivity state via :py:attr:`exclusive`\ .
        """
        if override is not None:
            new = override
        else:
            new = not self.exclusive
        self.exclusive = new
        self.set_exclusive_mouse(self.exclusive)
        self.peng.sendEvent("peng3d:window.toggle_exclusive",{"peng":self.peng,"window":self,"exclusive":self.exclusive})

    def set2d(self):
        """
        Configures OpenGL to draw in 2D.

        Note that wireframe mode is always disabled in 2D-Mode, but can be re-enabled by calling ``glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)``\ .
        """
        # Light

        glDisable(GL_LIGHTING)

        # To avoid accidental wireframe GUIs and fonts
        glPolygonMode( GL_FRONT_AND_BACK, GL_FILL)

        width, height = self.get_size()
        glDisable(GL_DEPTH_TEST)
        glViewport(0, 0, width, height)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(0, width, 0, height, -1, 1)
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
    def set3d(self,cam):
        """
        Configures OpenGL to draw in 3D.

        This method also applies the correct rotation and translation as set in the supplied camera ``cam``\ .
        It is discouraged to use :py:func:`glTranslatef()` or :py:func:`glRotatef()` directly as this may cause visual glitches.

        If you need to configure any of the standard parameters, see the docs about :doc:`/configoption`\ .

        The :confval:`graphics.wireframe` config value can be used to enable a wireframe mode, useful for debugging visual glitches.
        """
        if not isinstance(cam,camera.Camera):
            raise TypeError("cam is not of type Camera!")

        # Light

        #glEnable(GL_LIGHTING)

        if self.cfg["graphics.wireframe"]:
            glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)

        width, height = self.get_size()
        glEnable(GL_DEPTH_TEST)
        glViewport(0, 0, width, height)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(self.cfg["graphics.fieldofview"], width / float(height), self.cfg["graphics.nearclip"], self.cfg["graphics.farclip"]) # default 60
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        x, y = cam.rot
        glRotatef(x, 0, 1, 0)
        glRotatef(-y, math.cos(math.radians(x)), 0, math.sin(math.radians(x)))
        x, y, z = cam.pos
        glTranslatef(-x, -y, -z)