'''
Copyright (C) 2015 Taylor University, CG Cookie

Created by Dr. Jon Denning and Spring 2015 COS 424 class

Some code copied from CG Cookie Retopoflow project
https://github.com/CGCookie/retopoflow

    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 3 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, see <http://www.gnu.org/licenses/>.
'''

import bpy
import bgl

from mathutils import Vector, Matrix, Euler
import math
import time

from bpy.types import Operator
from bpy.types import SpaceView3D

from bpy_extras.view3d_utils import location_3d_to_region_2d, region_2d_to_vector_3d
from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_origin_3d

from .common.debug import debugger
from .common.utils import registered_object_add, registered_check

#from .lib.common_classes import TextBox
#from . import key_maps


class ModalOperator(Operator):

    def initialize(self, FSM=None):
        # make sure that the appropriate functions are defined!
        # note: not checking signature, though :(
        dfns = {
            'start_poll':       'start_poll(self,context)',
            'start':            'start(self,context)',
            'end':              'end(self,context)',
            'end_commit':       'end_commit(self,context)',
            'end_cancel':       'end_cancel(self,context)',
            'update':           'update(self,context)',
            'draw_postview':    'draw_postview(self,context)',
            'draw_postpixel':   'draw_postpixel(self,context)',
            'modal_wait':       'modal_wait(self,context,eventd)',
        }
        lbad = [fnname for fnname in dfns.keys() if not hasattr(self, fnname)]
        if lbad:
            print('Critical Error! Missing definitions for the following functions:')
            for fnname in lbad: print('  %s' % dfns[fnname])
            assert False, 'Modal operator missing definitions: %s' % ','.join(dfns[fnname] for fnname in lbad)

        self.events_nav = {'MIDDLEMOUSE', 'SHIFT+MIDDLEMOUSE','WHEELINMOUSE','WHEELOUTMOUSE', 'WHEELUPMOUSE','WHEELDOWNMOUSE'}
        self.FSM = {} if not FSM else dict(FSM)
        self.FSM['main'] = self.modal_main
        self.FSM['nav']  = self.modal_nav
        self.FSM['wait'] = self.modal_wait

        registered_object_add(self)

        self.initialized = True


    def get_event_details(self, context, event):
        '''
        Construct an event dictionary that is *slightly* more
        convenient than stringing together a bunch of logical
        conditions
        '''

        event_ctrl  = 'CTRL+'  if event.ctrl  else ''
        event_shift = 'SHIFT+' if event.shift else ''
        event_alt   = 'ALT+'   if event.alt   else ''
        event_oskey = 'OSKEY+' if event.oskey else ''
        event_ftype = event_ctrl + event_shift + event_alt + event_oskey + event.type

        event_pressure = 1 if not hasattr(event, 'pressure') else event.pressure

        return {
            'context': context,
            'region':  context.region,
            'r3d':     context.space_data.region_3d,

            'ctrl':    event.ctrl,
            'shift':   event.shift,
            'alt':     event.alt,
            'value':   event.value,
            'type':    event.type,
            'ftype':   event_ftype,
            'press':   event_ftype if event.value=='PRESS'   else None,
            'release': event_ftype if event.value=='RELEASE' else None,

            'mouse':   (float(event.mouse_region_x), float(event.mouse_region_y)),
            'pressure': event_pressure,
            }

    def handle_exception(self, serious=False):
        errormsg, errorhash = debugger.get_exception_info_and_hash()
        # if max number of exceptions occur within threshold of time, abort!
        print('\n',errormsg)
        curtime = time.time()
        self.exceptions_caught += [(errormsg, curtime)]
        # keep exceptions that have occurred within the last 5 seconds
        self.exceptions_caught = [(m,t) for m,t in self.exceptions_caught if curtime-t < 5]
        # if we've seen the same message before (within last 5 seconds), assume
        # that something has gone badly wrong
        c = sum(1 for m,t in self.exceptions_caught if m == errormsg)
        if serious or c > 1:
            print('\n'*5)
            print('-'*100)
            print('Something went wrong. Please start an error report with CG Cookie so we can fix it!')
            print('-'*100)
            print('\n'*5)
            #show_error_message('Something went wrong. Please start an error report with CG Cookie so we can fix it!', wrap=240)
            self.exception_quit = True
        
        self.fsm_mode = 'main'
        pass
    
    ####################################################################
    # Draw handler function

    def draw_callback_postview(self, context):
        if not registered_check(): return
        bgl.glPushAttrib(bgl.GL_ALL_ATTRIB_BITS)    # save OpenGL attributes
        try: self.draw_postview(context)
        except: self.handle_exception()
        bgl.glPopAttrib()                           # restore OpenGL attributes

    def draw_callback_postpixel(self, context):
        if not registered_check(): return
        bgl.glPushAttrib(bgl.GL_ALL_ATTRIB_BITS)    # save OpenGL attributes
        try: self.draw_postpixel(context)
        except: self.handle_exception()
        bgl.glPopAttrib()                           # restore OpenGL attributes


    ####################################################################
    # FSM modal functions

    def modal_nav(self, context, eventd):
        '''
        Determine/handle navigation events.
        FSM passes control through to underlying panel if we're in 'nav' state
        '''
 
        handle_nav = False
        handle_nav |= eventd['ftype'] in self.events_nav

        if handle_nav:
            self.post_update   = True
            self.is_navigating = True
            return 'main' if eventd['value']=='RELEASE' else 'nav'

        self.is_navigating = False
        return ''

    def modal_main(self, context, eventd):
        '''
        Main state of FSM.
        This state checks if navigation is occurring.
        This state calls auxiliary wait state to see into which state we transition.
        '''

        # handle general navigationvrot = context.space_data.region_3d.view_rotation
        nmode = self.FSM['nav'](context, eventd)
        if nmode: return nmode

        # accept / cancel
        if eventd['press'] in {'RET', 'NUMPAD_ENTER'}:
            # commit the operator
            # (e.g., create the mesh from internal data structure)
            return 'finish'
        if eventd['press'] in {'ESC'}:
            # cancel the operator
            return 'cancel'

        # handle general waiting
        try:
            nmode = self.FSM['wait'](context, eventd)
            if nmode: return nmode
        except:
            self.handle_exception()
            return ''

        return ''


    def modal_start(self, context):
        '''
        get everything ready to be run as modal tool
        '''
        self.fsm_mode      = 'main'
        self.mode_pos      = (0, 0)
        self.cur_pos       = (0, 0)
        self.is_navigating = False
        self.cb_pv_handle  = SpaceView3D.draw_handler_add(self.draw_callback_postview, (context, ), 'WINDOW', 'POST_VIEW')
        self.cb_pp_handle  = SpaceView3D.draw_handler_add(self.draw_callback_postpixel, (context, ), 'WINDOW', 'POST_PIXEL')
        context.window_manager.modal_handler_add(self)
        #context.area.header_text_set(self.bl_label)

        self.footer = ''
        self.footer_last = ''
        
        try: self.start(context)
        except: self.handle_exception()

    def modal_end(self, context):
        '''
        finish up stuff, as our tool is leaving modal mode
        '''
        try: self.end(context)
        except: self.handle_exception()
        SpaceView3D.draw_handler_remove(self.cb_pv_handle, "WINDOW")
        SpaceView3D.draw_handler_remove(self.cb_pp_handle, "WINDOW")
        context.area.header_text_set()

    def modal(self, context, event):
        '''
        Called by Blender while our tool is running modal.
        This is the heart of the finite state machine.
        '''
        
        # if something bad happened, bail!
        if not registered_check() or self.exception_quit:
            print('Something bad happened, so we are bailing!')
            self.modal_end(context)
            context.area.tag_redraw()
            return {'CANCELLED'}
        
        if not context.area: return {'RUNNING_MODAL'}

        context.area.tag_redraw()       # force redraw

        eventd = self.get_event_details(context, event)

        try:
            self.cur_pos  = eventd['mouse']
            nmode = self.FSM[self.fsm_mode](context, eventd)
            self.mode_pos = eventd['mouse']
        except:
            self.handle_exception()
            return {'RUNNING_MODAL'}

        if nmode == 'wait': nmode = 'main'

        self.is_navigating = (nmode == 'nav')
        if nmode == 'nav':
            return {'PASS_THROUGH'}     # pass events (mouse,keyboard,etc.) on to region

        if nmode in {'finish','cancel'}:
            if nmode == 'finish':
                self.end_commit(context)
            else:
                self.end_cancel(context)
            self.modal_end(context)
            return {'FINISHED'} if nmode == 'finish' else {'CANCELLED'}

        if nmode: self.fsm_mode = nmode

        return {'RUNNING_MODAL'}    # tell Blender to continue running our tool in modal

    def invoke(self, context, event):
        '''
        called by Blender when the user invokes (calls/runs) our tool
        '''
        
        assert getattr(self, 'initialized', False), 'Must initialize operator before invoking'
        
        if not registered_check(): return {'CANCELLED'}
        
        #print('clearing exceptions')
        self.exceptions_caught = []
        self.exception_quit = False
        
        try:
            if not self.start_poll(context):    # can the tool get started?
                return {'CANCELLED'}
        except:
            self.handle_exception()
            return {'CANCELLED'}
        
        self.modal_start(context)
        
        return {'RUNNING_MODAL'}    # tell Blender to continue running our tool in modal