# encoding: utf-8
 
import time
 
import flappy
from flappy import _core
from flappy.events import Event, KeyboardEvent, MouseEvent, TouchEvent
from flappy.events import FocusEvent
from flappy.geom import Point
from flappy.display import DisplayObject, DisplayObjectContainer
 
 
class StageQuality(object):
    LOW      = 'low'
    MEDIUM   = 'medium'
    HIGH     = 'high'
    BEST     = 'best'
    _ENUM    = [LOW, MEDIUM, HIGH, BEST]
 
 
class Stage(DisplayObjectContainer, _core._Stage):
 
#public constants
    DOUBLE_CLICK_INTERVAL = 0.25
 
#public methods
    def invalidate(self):
        self._invalid = True
 
#public properties
    @property
    def frameRate(self):
        return self._frame_rate
 
    @frameRate.setter
    def frameRate(self, value):
        self._frame_rate = value
        self._frame_period = value if value <= 0 else 1.0 / value
 
    @property
    def stage(self):
        return self
 
    @property
    def stageWidth(self):
        return self.getStageWidth()
 
    @property
    def stageHeight(self):
        return self.getStageHeight()
 
    @property
    def focus(self):
        obj_id = self._get_focus_id()
        return self._find_by_id(obj_id)
 
    @focus.setter
    def focus(self, focus_obj):
        self._set_focus(focus_obj)
 
    @property
    def quality(self):
        return StageQuality._ENUM[self.getQuality()]
 
    @quality.setter
    def quality(self, quality):
        self.setQuality(StageQuality._ENUM.index(quality))
 
    @property
    def color(self):
        return self.opaqueBackground
 
    @color.setter
    def color(self, color):
        self.opaqueBackground = color
 
#private constants
    _EF_LEFT_DOWN       = 0x0001
    _EF_SHIFT_DOWN      = 0x0002
    _EF_CTRL_DOWN       = 0x0004
    _EF_ALT_DOWN        = 0x0008
    _EF_COMMAND_DOWN    = 0x0010
    _EF_LOCATION_RIGHT  = 0x4000
    _EF_NO_NATIVE_CLICK = 0x10000
 
    _MOUSE_CHANGES = (
        MouseEvent.MOUSE_OUT,
        MouseEvent.MOUSE_OVER,
        MouseEvent.ROLL_OUT,
        MouseEvent.ROLL_OVER
    )
 
    _TOUCH_CHANGES = (
        TouchEvent.TOUCH_OUT,
        TouchEvent.TOUCH_OVER,
        TouchEvent.TOUCH_ROLL_OUT,
        TouchEvent.TOUCH_ROLL_OVER
    )
 
    _S_CLICK_EVENTS = (
        MouseEvent.CLICK,
        MouseEvent.MIDDLE_CLICK,
        MouseEvent.RIGHT_CLICK
    )
 
    _S_DOWN_EVENTS = (
        MouseEvent.MOUSE_DOWN,
        MouseEvent.MIDDLE_MOUSE_DOWN,
        MouseEvent.RIGHT_MOUSE_DOWN
    )
 
    _S_UP_EVENTS = (
        MouseEvent.MOUSE_UP,
        MouseEvent.MIDDLE_MOUSE_UP,
        MouseEvent.RIGHT_MOUSE_UP
    )
 
    _EARLY_WAKE_UP = 0.005
 
#private class variables
    _current_stage = None
 
#private methods
    @classmethod
    def _get_instance(cls):
        if not cls._current_stage:
            cls._current_stage = cls()
        return cls._current_stage
 
    def _native_init(self):
        _core._Stage.__init__(self)
 
    def __init__(self):
        DisplayObjectContainer.__init__(self, "Stage")
        _core._set_event_handler(self._process_stage_event)
 
        self._mouse_over_objects = []
        self._focus_over_objects = []
        self.active = True
 
        self._invalid = False
        self._last_render = 0.0
        self._last_down = [None, None, None]
        self._last_click_time = 0.0
        self._touch_info = {}
        self._joy_axis_data = {}
        self._drag_bounds = None
        self._frame_rate = 0
        self._frame_period = 0
 
        self._drag_object = None
        self._drag_offset_x = 0.0
        self._drag_offset_y = 0.0
 
        self.frameRate = 100
 
        if _core._request_render:
            _core._request_render()
 
        self._event_map = {
            _core.etKeyDown:
                lambda event:
                    self._on_key(event, KeyboardEvent.KEY_DOWN),
 
            _core.etKeyUp:
                lambda event:
                    self._on_key(event, KeyboardEvent.KEY_UP),
 
            _core.etMouseMove:
                lambda event:
                    self._on_mouse(event, MouseEvent.MOUSE_MOVE, True),
 
            _core.etMouseDown:
                lambda event:
                    self._on_mouse(event, MouseEvent.MOUSE_DOWN, True),
 
            _core.etMouseUp:
                lambda event:
                    self._on_mouse(event, MouseEvent.MOUSE_UP, True),
 
            _core.etMouseClick:
                lambda event:
                    self._on_mouse(event, MouseEvent.CLICK, True),
 
            _core.etResize:
                lambda event:
                    self._on_resize(),
 
            _core.etPoll:
                lambda event:
                    self._poll_timers(),
 
            _core.etQuit:
                lambda event:
                    self._on_quit(),
 
            _core.etFocus:
                self._on_focus,
 
            _core.etShouldRotate:
                self._should_rotate,
 
            _core.etDestroyHandler:
                lambda event: None,
 
            _core.etRedraw:
                lambda event:
                    self._render(True),
 
            _core.etTouchBegin:
                self._on_touch_begin,
 
            _core.etTouchMove:
                self._on_touch_move,
 
            _core.etTouchEnd:
                self._on_touch_end,
 
            _core.etChange:
                self._on_change,
 
            _core.etActivate:
                lambda event:
                    self._set_active(True),
 
            _core.etDeactivate:
                lambda event:
                    self._set_active(False),
 
            _core.etGotInputFocus:
                lambda event:
                    self._on_got_input_focus(),
 
            _core.etLostInputFocus:
                lambda event:
                    self._on_lost_input_focus(),
 
            _core.etJoyAxisMove:
                lambda event:
                    self._on_joystick(event, 0),
 
            _core.etJoyBallMove:
                lambda event:
                    self._on_joystick(event, 0),
 
            _core.etJoyHatMove:
                lambda event:
                    self._on_joystick(event, 0),
 
            _core.etJoyButtonDown:
                lambda event:
                    self._on_joystick(event, 0),
 
            _core.etJoyButtonUp:
                lambda event:
                    self._on_joystick(event, 0),
        }
 
    def _process_stage_event(self, event):
        self._event_map[event.type](event)
        self._update_next_wake()
 
    def _check_render(self):
        if self.frameRate > 0:
            now = time.time()
            if now >= (self._last_render + self._frame_period):
                self._last_render = now
 
                if _core._request_render:
                    _core._request_render()
                else:
                    self._render(True)
 
    def _render(self, send_enterframe):
        if not self.active:
            return
        if send_enterframe:
            self._broadcast(Event(Event.ENTER_FRAME))
        if self._invalid:
            self._invalid = False
            self._broadcast(Event(Event.RENDER))
        self._render_stage()
 
    def _on_quit(self):
        flappy.stop()
 
    def _on_key(self, event, etype):
        stack = []
        obj = self._find_by_id(event.id)
        if obj is not None:
            obj._get_interactive_object_stack(stack)
 
        if stack:
            value = event.value
            if ord('a') <= value <= ord('z'):
                value -= (ord('a') - ord('A'))
 
            obj = stack[0]
            flags = event.flags
            key_location = 1 if (flags & self._EF_LOCATION_RIGHT ) else 0
            ctrl_down = (flags & self._EF_CTRL_DOWN) != 0
            alt_down = (flags & self._EF_ALT_DOWN) != 0
            shift_down = (flags & self._EF_SHIFT_DOWN) != 0
 
            evt = KeyboardEvent(etype,
                bubbles=True, cancelable=True, charCodeValue=event.code,
                    keyCodeValue=value, keyLocationValue=key_location,
                        ctrlKeyValue=ctrl_down, altKeyValue=alt_down,
                            shiftKeyValue=shift_down)
            obj._fire_event(evt)
 
    def _on_change(self, event):
        obj = self._find_by_id(event.id)
        if obj is not None:
            obj._fire_event(Event(Event.CHANGE))
 
    def _on_got_input_focus(self):
        evt = Event(FocusEvent.FOCUS_IN)
        self._dispatch_event(evt)
 
    def _on_lost_input_focus(self):
        evt = Event(FocusEvent.FOCUS_OUT)
        self._dispatch_event(evt)
 
    def _on_focus(self, event):
        stack = []
        obj = self._find_by_id(event.id)
 
        if obj is not None:
            obj._get_interactive_object_stack(stack)
 
        if stack and (event.value == 1 or event.value == 2):
            obj = stack[0]
            if event.value == 1:
                etype = FocusEvent.MOUSE_FOCUS_CHANGE
            else:
                etype = FocusEvent.KEY_FOCUS_CHANGE
            relobj = None
            if self._focus_over_objects:
                relobj = self._focus_over_objects[0]
 
            evt = FocusEvent(etype,
                    bubbles=True, cancelable=True, relatedObject=relobj,
                        shiftKey=(event.flags > 0), keyCode=event.code)
            obj._fire_event(evt)
            if evt.isCancelled:
                event.result = 1
 
        stack.reverse()
        self._checkFocusInOuts(event, stack)
 
    def _checkFocusInOuts(self, event, stack):
        new_n = len(stack)
        new_obj = stack[-1] if stack else None
        old_n = len(self._focus_over_objects)
        if self._focus_over_objects:
            old_obj = self._focus_over_objects[-1]
        else:
            old_obj = None
 
        if new_obj != old_obj:
            common = 0
            while (common < new_n) and \
                    (common < old_n) and \
                        (stack[common] == self._focus_over_objects[common]):
                common += 1
 
            fout = FocusEvent(FocusEvent.FOCUS_OUT,
                        bubbles=False, cancelable=False, relatedObject=new_obj,
                            shiftKey=(event.flags > 0), keyCode=event.code)
            i = old_n - 1
            while i >= common:
                self._focus_over_objects[i]._dispatch_event(fout)
                i -= 1
 
            fin = FocusEvent(FocusEvent.FOCUS_IN,
                        bubbles=False, cancelable=False, relatedObject=old_obj,
                            shiftKey=(event.flags > 0), keyCode=event.code)
            i = new_n - 1
            while i >= common:
                stack[i]._dispatch_event(fin)
                i -= 1
 
            self._focus_over_objects = stack
 
 
    def _on_mouse(self, event, event_type, from_mouse):
        etype = event_type
        button = event.value
        if not from_mouse:
            button = 0
 
        wheel = 0
        if event_type == MouseEvent.MOUSE_DOWN:
            if button > 2:
                return
            etype = Stage._S_DOWN_EVENTS[button]
 
        elif event_type == MouseEvent.MOUSE_UP:
            if button > 2:
                etype = MouseEvent.MOUSE_WHEEL
                wheel = 1 if button == 3 else -1
            else:
                etype = Stage._S_UP_EVENTS[button]
 
        if self._drag_object != None:
            self._drag(Point(event.x, event.y))
 
        stack = []
        obj = self._find_by_id(event.id)
        if obj is not None:
            obj._get_interactive_object_stack(stack)
 
        local = None
        if stack:
            obj = stack[0]
            stack.reverse()
            local = obj.globalToLocal(Point(event.x, event.y))
            evt = MouseEvent._create(etype, event, local, obj)
            evt.delta = wheel
            if from_mouse:
                self._check_in_outs(evt, stack)
            obj._fire_event(evt)
        else:
            local = Point(event.x, event.y)
            evt = MouseEvent._create(etype, event, local, None)
            evt.delta = wheel
            if from_mouse:
                self._check_in_outs(evt, stack)
 
        click_obj = stack[-1] if len(stack) else self
        if event_type == MouseEvent.MOUSE_DOWN and button < 3:
            self._last_down[button] = click_obj
 
        elif event_type == MouseEvent.MOUSE_UP and button < 3:
            if click_obj == self._last_down[button]:
                evt = MouseEvent._create(Stage._S_CLICK_EVENTS[button],
                                            event, local, click_obj)
                click_obj._fire_event(evt)
                if button == 0 and click_obj.doubleClickEnabled:
                    now = time.time()
                    diff = now - self._last_click_time
                    if diff <= self.DOUBLE_CLICK_INTERVAL:
                        evt = MouseEvent._create(MouseEvent.DOUBLE_CLICK,
                                                    event, local, click_obj)
                        click_obj._fire_event(evt)
                    self._last_click_time = now
 
            self._last_down[button] = None
 
 
    def _check_in_outs(self, event, stack, touch_info=None):
        if touch_info is not None:
            prev = touch_info.touchOverObjects
            mevents = self._TOUCH_CHANGES
        else:
            prev = self._mouse_over_objects
            mevents = self._MOUSE_CHANGES
 
        new_n = len(stack)
        new_obj = stack[-1] if new_n else None
        old_n = len(prev)
        old_obj = prev[-1] if old_n else None
 
        if new_obj != old_obj:
            if old_obj is not None:
                nevent = event._create_similar(mevents[0], new_obj, old_obj)
                old_obj._fire_event(nevent)
            if new_obj != None:
                nevent = event._create_similar(mevents[1], old_obj)
                new_obj._fire_event(nevent)
 
            common = 0
            while (common < new_n) and \
                    (common < old_n) and \
                        (stack[common] == prev[common]):
                common += 1
 
            nevent = event._create_similar(mevents[2], new_obj, old_obj)
            i = old_n - 1
            while i >= common:
                prev[i]._dispatch_event(nevent)
                i -= 1
 
            nevent = event._create_similar(mevents[3], old_obj)
            i = new_n - 1
            while i >= common:
                stack[i]._dispatch_event(nevent)
                i -= 1
 
            if touch_info:
                touch_info.touchOverObjects = stack
            else:
                self._mouse_over_objects = stack
 
    def _on_touch_begin(self, event):
        touch_info = _TouchInfo()
        self._touch_info[event.value] = touch_info
        self._on_touch(event, TouchEvent.TOUCH_BEGIN, touch_info)
 
        if event.flags & TouchEvent.efPrimaryTouch:
            self._on_mouse(event, MouseEvent.MOUSE_DOWN, False)
 
    def _on_touch_move(self, event):
        touch_info = self._touch_info[event.value]
        self._on_touch(event, TouchEvent.TOUCH_MOVE, touch_info)
 
    def _on_touch_end(self, event):
        touch_info = self._touch_info[event.value]
        self._on_touch(event, TouchEvent.TOUCH_END, touch_info)
        del self._touch_info[event.value]
 
        if event.flags & TouchEvent.efPrimaryTouch:
            self._on_mouse(event, MouseEvent.MOUSE_UP, False)
 
    def _on_touch(self, event, etype, touch_info):
        stack = []
 
        obj = self._find_by_id(event.id)
 
        if obj is not None:
            obj._get_interactive_object_stack(stack)
 
        if stack:
            obj = stack[0]
            stack.reverse()
            local = obj.globalToLocal(Point(event.x, event.y))
            evt = TouchEvent._create(etype, event, local, obj,
                                        event.sx, event.sy)
            evt.touchPointID = event.value
            evt.isPrimaryTouchPoint = \
                                bool(event.flags & TouchEvent.efPrimaryTouch)
            self._check_in_outs(evt, stack, touch_info)
            obj._fire_event(evt)
 
            if evt.isPrimaryTouchPoint and etype == TouchEvent.TOUCH_MOVE:
                if self._drag_object:
                    self._drag(Point(event.x, event.y))
                evt = MouseEvent._create(MouseEvent.MOUSE_MOVE, event,
                                            local, obj)
                obj._fire_event(evt)
        else:
            evt = TouchEvent._create(etype, event, Point(event.x, event.y),
                                        None, event.sx, event.sy)
            evt.touchPointID = event.value
            evt.isPrimaryTouchPoint = \
                                bool(event.flags & TouchEvent.efPrimaryTouch)
            self._check_in_outs(evt, stack, touch_info)
 
    def _drag(self, mouse):
        parent = self._drag_object.parent
        if parent is not None:
            mouse = parent.globalToLocal(mouse)
 
        dragobj_x = mouse.x - self._drag_offset_x
        dragobj_y = mouse.y - self._drag_offset_y
 
        if self._drag_bounds:
            if dragobj_x < self._drag_bounds.x:
                dragobj_x = self._drag_bounds.x
            elif dragobj_x > self._drag_bounds.right:
                dragobj_x = self._drag_bounds.right
 
            if dragobj_y < self._drag_bounds.y:
                dragobj_y = self._drag_bounds.y
            elif dragobj_y > self._drag_bounds.bottom:
                dragobj_y = self._drag_bounds.bottom
 
        self._drag_object.x = dragobj_x
        self._drag_object.y = dragobj_y
 
    def _on_joystick(self, event, event_type):
        raise NotImplementedError
 
    def _should_rotate(self, event):
        raise NotImplementedError
 
    def _start_drag(self, obj, lock_center, bounds):
        self._drag_bounds = None
        if bounds:
            self._drag_bounds = bounds.clone()
 
        self._drag_object = obj
 
        if lock_center:
            self._drag_offset_x = -obj.width * 0.5
            self._drag_offset_y = -obj.height * 0.5
        else:
            mouse = Point(self.mouseX, self.mouseY)
            parent = self._drag_object.parent
            if parent is not None:
                mouse = parent.globalToLocal(mouse)
            self._drag_offset_x = self._drag_object.x - mouse.x
            self._drag_offset_y = self._drag_object.y - mouse.y
 
 
    def _stop_drag(self, obj):
        self._drag_bounds = None
        self._drag_object = None
 
    def _on_resize(self):
        event = Event(Event.RESIZE)
        self._broadcast(event)
 
        if _core._request_render is None:
            self._render(False)
 
    def _poll_timers(self):
        self._check_render()
 
    def _set_active(self, active):
        if active != self.active:
            self.active = active
            if not active:
                self._last_render = time.time()
 
            event = Event(Event.ACTIVATE if active else Event.DEACTIVATE)
            self._broadcast(event)
            if active:
                self._poll_timers()
 
    def _update_next_wake(self):
        next_wake = self._next_frame_due(60.0)
        self._set_next_wake_delay(next_wake)
 
    def _next_frame_due(self, other_timers):
        if not self.active:
            return other_timers
 
        if self.frameRate > 0:
            next = self._last_render - time.time()
            next += (self._frame_period - Stage._EARLY_WAKE_UP)
            if next < other_timers:
                return next
 
        return other_timers
 
 
class _TouchInfo(object):
    def __init__(self):
        self.touchOverObjects = []