#!/opt/local/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
# -*- mode: python; make-backup-files: nil -*-

"""\
usage: view3ds [options] <3dsfile>
       view3ds [options] <zipfile>:<3dsfile>"""


from __future__ import division

import sys, os, math, optparse, cStringIO, zipfile, ctypes

import Dice3DS
from Dice3DS import dom3ds, util
from Dice3DS.example import config


# Load a model from the filesystem

def load_model_from_filesystem(modfilename,options):
    texdir = os.path.dirname(modfilename)
    if options.imageloader == "none":
        def load_texture(texfilename):
            return gltexture.NonTexture()
    else:
        def load_texture(texfilename):
            try:
                return gltexture.Texture(os.path.join(texdir,texfilename))
            except IOError,e:
                e.strerror = e.strerror + ' (use "-i none" to disable images)'
                raise
    dom = dom3ds.read_3ds_file(modfilename,tight=options.tight,
                               recover=options.recover)
    if options.recover:
        dom3ds.remove_errant_chunks(dom)
    return glmodel.GLModel(dom,load_texture,options.nfunc,options.frameno)


# Load a model from the zipfile

def load_model_from_zipfile(zipname,modarcname,options):
    if options.imageloader == "none":
        def load_texture(texfilename):
            return gltexture.NonTexture()
    else:
        def load_texture(texfilename):
            try:
                return gltexture.Texture((zfo,texfilename))
            except KeyError,e:
                e.args = (e.args[-1] + ' (use "-i none" to disable images)',)
                raise
    zfo = zipfile.ZipFile(zipname,"r")
    try:
        dom = dom3ds.read_3ds_mem(zfo.read(modarcname),tight=options.tight,
                                  recover=options.recover)
        if options.recover:
            dom3ds.remove_errant_chunks(dom)
        return glmodel.GLModel(dom,load_texture,options.nfunc,options.frameno)
    finally:
        zfo.close()


# Utility function to get window size from option

def scan_window_size(sizeopt):
    def formaterror():
        raise ValueError("window size option invalid format, "
                         "must be <integer>x<integer>")        
    s = sizeopt.split("x")
    if len(s) != 2:
        formaterror()
    try:
        width = int(s[0].strip())
        height = int(s[1].strip())
    except ValueError:
        formaterror()
    return width,height


# Prerender the model (i.e., create a display list)

def prerender_model(filename,options):

    # Print helpful message at terminal

    print >> sys.stderr, "Loading model...."
    if options.normals != "none":
        print >> sys.stderr, "(If it's taking too long, try running command "
        print >> sys.stderr, 'with "-n none" to shut off normals calculation.)'

    # Load model

    if ':' in filename:
        zipfile,arcname = filename.split(':',1)
        model = load_model_from_zipfile(zipfile,arcname,options)
    else:
        model = load_model_from_filesystem(filename,options)

    # Create the display list

    dl = model.create_dl()

    # Finish helpful message.
    
    print >> sys.stderr, "Model created."

    # Calculate bounding parameters

    ul,ll = model.bounding_box()
    center = tuple(float(x) for x in (ul+ll)/2)
    scale = 1.0/max(abs(ul-ll))

    # Return pertinent information

    return dl,center,scale


# GLUT frontend

def run_glut(filename,options):

    # Import GLUT

    from OpenGL import GLUT

    # Start GLUT

    GLUT.glutInit(())
    GLUT.glutInitDisplayMode(GLUT.GLUT_RGBA|GLUT.GLUT_DOUBLE|GLUT.GLUT_DEPTH)
    GLUT.glutInitWindowSize(*scan_window_size(options.windowsize))
    GLUT.glutCreateWindow("3DS Viewer: %s" % filename)

    # Load model

    dl,center,scale = prerender_model(filename,options)
 
    # Set up state variables

    class State(object):
        pass
    state = State()
    state.distance = 1.5
    state.azimuth = 45.0
    state.altitude = 10.0
    state.swiveling = False
    state.zooming = False

    # Initialize OpenGL

    glEnable(GL_DEPTH_TEST)
    glEnable(GL_LIGHTING)
    glEnable(GL_LIGHT0)
    glEnable(GL_NORMALIZE)
    glShadeModel(GL_SMOOTH)
    glClearColor(0.5,0.5,1.0,0)

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45.0,640/480.0,0.1,1000.0)
    glMatrixMode(GL_MODELVIEW)

    # Set light intensity

    amb = options.ambience
    dif = 1.0-amb
    spc = options.specularity
    glLightfv(GL_LIGHT0, GL_AMBIENT, gl_float_array(amb, amb, amb, 1.0))
    glLightfv(GL_LIGHT0, GL_DIFFUSE, gl_float_array(dif, dif, dif, 1.0))
    glLightfv(GL_LIGHT0, GL_SPECULAR, gl_float_array(spc, spc, spc, 1.0))

    # Event handlers

    def on_expose():
        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

        glLoadIdentity()

        glLightfv(GL_LIGHT0, GL_POSITION, gl_float_array(1.0,1.0,10.0,0))

        glTranslatef(0.0, 0.0, -state.distance)
        glRotatef(180.0, 0.0, 0.0, 1.0)
        glRotatef(90.0, 1.0, 0.0, 0.0)
        glRotatef(state.altitude, -1.0, 0.0, 0.0)
        glRotatef(state.azimuth, 0.0, 0.0, 1.0)

        glScalef(scale,scale,scale)
        glTranslatef(*(-x for x in center))

        glCallList(dl)

        GLUT.glutSwapBuffers()

    def on_mouse_button(button,event,x,y):
        if event == GLUT.GLUT_DOWN:
            if button == GLUT.GLUT_LEFT_BUTTON:
                state.x0 = x
                state.y0 = y
                state.alt0 = state.altitude
                state.az0 = state.azimuth
                state.swiveling = True
                state.zooming = False
            elif button == GLUT.GLUT_MIDDLE_BUTTON:
                state.x0 = x
                state.y0 = y
                state.dist0 = state.distance
                state.zooming = True
                state.swiveling = False
        elif event == GLUT.GLUT_UP:
            if button == GLUT.GLUT_LEFT_BUTTON:
                state.swiveling = False
            elif button == GLUT.GLUT_MIDDLE_BUTTON:
                state.zooming = False

    def on_mouse_drag(x,y):
        if state.swiveling:
            state.azimuth = state.az0+(x-state.x0)
            state.altitude = max(min(state.alt0-(state.y0-y),90.0),-90.0)
            GLUT.glutPostRedisplay()
        elif state.zooming:
            state.distance = state.dist0*math.exp(-(state.y0-y)/100.0)
            GLUT.glutPostRedisplay()

    def on_key_press(key,x,y):
        if key in "qQ\x1B":
            sys.exit(0)

    GLUT.glutDisplayFunc(on_expose)
    GLUT.glutMouseFunc(on_mouse_button)
    GLUT.glutMotionFunc(on_mouse_drag)
    GLUT.glutKeyboardFunc(on_key_press)

    # Main loop

    GLUT.glutMainLoop()


# Pygame frontend

def run_pygame(filename,options):

    # Import Pygame

    import pygame

    # Start Pygame

    pygame.init()
    pygame.display.set_mode(scan_window_size(options.windowsize),
                            pygame.OPENGL|pygame.DOUBLEBUF)
    pygame.display.set_caption("3DS Viewer: %s" % filename)

    # Load model

    dl,center,scale = prerender_model(filename,options)
 
    # Set up state variables

    distance = 1.5
    azimuth = 45.0
    altitude = 10.0
    
    swiveling = False
    zooming = False
    
    # Initialize OpenGL

    glEnable(GL_DEPTH_TEST)
    glEnable(GL_LIGHTING)
    glEnable(GL_LIGHT0)
    glEnable(GL_NORMALIZE)
    glShadeModel(GL_SMOOTH)
    glClearColor(0.5,0.5,1.0,0)

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45.0,640/480.0,0.1,1000.0)
    glMatrixMode(GL_MODELVIEW)

    # Set light intensity

    amb = options.ambience
    dif = 1.0-amb
    spc = options.specularity
    glLightfv(GL_LIGHT0, GL_AMBIENT, gl_float_array(amb, amb, amb, 1.0))
    glLightfv(GL_LIGHT0, GL_DIFFUSE, gl_float_array(dif, dif, dif, 1.0))
    glLightfv(GL_LIGHT0, GL_SPECULAR, gl_float_array(spc, spc, spc, 1.0))

    # Event generator

    class fire_events(object):
        def __iter__(self):
            return self
        def next(self):
            return pygame.event.wait()

    # Main loop

    for event in fire_events():
        redraw = False
        if event.type in (pygame.VIDEOEXPOSE,pygame.VIDEORESIZE):
            redraw = True
        if event.type == pygame.MOUSEBUTTONDOWN:            
            if event.button == 1 and not zooming:
                pygame.event.set_grab(1)
                x0 = event.pos[0]
                y0 = event.pos[1]
                alt0 = altitude
                az0 = azimuth
                swiveling = True
            elif event.button == 2 and not swiveling:
                pygame.event.set_grab(1)
                x0 = event.pos[0]
                y0 = event.pos[1]
                dist0 = distance
                zooming = True
        elif event.type == pygame.MOUSEBUTTONUP:
            if event.button == 1:
                pygame.event.set_grab(0)
                swiveling = False
            elif event.button == 2:
                pygame.event.set_grab(0)
                zooming = False
        elif event.type == pygame.MOUSEMOTION:
            xe = pygame.event.get(pygame.MOUSEMOTION)
            if xe:
                event = xe[-1]
            if swiveling:
                x = event.pos[0]
                y = event.pos[1]
                azimuth = az0+(x-x0)
                altitude = max(min(alt0+(y-y0),90.0),-90.0)
                redraw = True
            if zooming:
                y = event.pos[1]
                distance = dist0*math.exp((y-y0)/100.0)
                redraw = True
        elif event.type == pygame.KEYDOWN:
            if event.key in (pygame.K_ESCAPE,pygame.K_q):
                sys.exit(0)
        elif event.type == pygame.QUIT:
            sys.exit(0)

        if not redraw:
            continue

        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

        glLoadIdentity()

        glLightfv(GL_LIGHT0, GL_POSITION, gl_float_array(1.0,1.0,10.0,0))

        glTranslatef(0.0, 0.0, -distance)
        glRotatef(180.0, 0.0, 0.0, 1.0)
        glRotatef(90.0, 1.0, 0.0, 0.0)
        glRotatef(altitude, -1.0, 0.0, 0.0)
        glRotatef(azimuth, 0.0, 0.0, 1.0)

        glScalef(scale,scale,scale)
        glTranslatef(*(-x for x in center))

        glCallList(dl)

        pygame.display.flip()


# Pyglet frontend

def run_pyglet(filename,options):

    # Import modules for pyglet

    import pyglet.window
    import pyglet.clock

    # Start pyglet

    window = pyglet.window.Window(
        visible=False,caption="3DS Viewer: %s" % filename,
        *scan_window_size(options.windowsize))
    pyglet.clock.set_fps_limit(30)

    # Load model

    dl,center,scale = prerender_model(filename,options)

    # Set up state variables

    class State(object):
        pass
    state = State()
    state.distance = 1.5
    state.azimuth = 45.0
    state.altitude = 10.0

    # Event handlers

    @window.event
    def on_show():
        glEnable(GL_DEPTH_TEST)
        glEnable(GL_LIGHTING)
        glEnable(GL_LIGHT0)
        glEnable(GL_NORMALIZE)
        glShadeModel(GL_SMOOTH)
        glClearColor(0.5,0.5,1.0,0)

        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(45.0,640/480.0,0.1,1000.0)
        glMatrixMode(GL_MODELVIEW)

        amb = options.ambience
        dif = 1.0-amb
        spc = options.specularity
        glLightfv(GL_LIGHT0, GL_AMBIENT, gl_float_array(amb, amb, amb, 1.0))
        glLightfv(GL_LIGHT0, GL_DIFFUSE, gl_float_array(dif, dif, dif, 1.0))
        glLightfv(GL_LIGHT0, GL_SPECULAR, gl_float_array(spc, spc, spc, 1.0))

    @window.event
    def on_expose():
        state.redraw = True

    @window.event
    def on_mouse_press(x,y,button,modifiers):
        if button == 1:
            state.x0 = x
            state.y0 = y
            state.alt0 = state.altitude
            state.az0 = state.azimuth
        elif button == 2:
            state.x0 = x
            state.y0 = y
            state.dist0 = state.distance

    @window.event
    def on_mouse_drag(x,y,dx,dy,buttons,modifiers):
        if buttons == 1:
            state.azimuth = state.az0+(x-state.x0)
            state.altitude = max(min(state.alt0-(y-state.y0),90.0),-90.0)
            state.redraw = True
        elif buttons == 2:
            state.distance = state.dist0*math.exp(-(y-state.y0)/100.0)
            state.redraw = True

    @window.event
    def on_key_press(symbol, modifiers):
        if symbol in (pyglet.window.key.Q,pyglet.window.key.ESCAPE):
            window.has_exit = True

    # Main loop

    window.set_visible()
    while not window.has_exit:
        state.redraw = False
        
        window.dispatch_events()
        pyglet.clock.tick()

        if not state.redraw:
            continue

        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

        glLoadIdentity()

        glLightfv(GL_LIGHT0, GL_POSITION, gl_float_array(1.0,1.0,10.0,0))

        glTranslatef(0.0, 0.0, -state.distance)
        glRotatef(180.0, 0.0, 0.0, 1.0)
        glRotatef(90.0, 1.0, 0.0, 0.0)
        glRotatef(state.altitude, -1.0, 0.0, 0.0)
        glRotatef(state.azimuth, 0.0, 0.0, 1.0)

        glScalef(scale,scale,scale)
        glTranslatef(*(-x for x in center))

        glCallList(dl)

        window.flip()


# Main program

def main():

    # Parse command line

    parser = optparse.OptionParser(
        usage=__doc__,
        version=".".join([str(x) for x in Dice3DS.version]))
    parser.add_option("-w","--window",
                      type="string",default="640x480",dest="windowsize",
                      help="Window size in pixels: WIDTHxHEIGHT")
    parser.add_option("-g","--gui",
                      type="string",default="any",dest="gui",
                      help="GUI library to use (glut,pyglet,pygame,any)")
    parser.add_option("-n","--normals",
                      type="string",default="cross",dest="normals",
                      help="normals calculation method (none,cross,angle)")
    parser.add_option("-f","--frame",
                      type="float",default=0,dest="frameno",
                      help="frame number (default 0, -1 to ignore keyframes)")
    parser.add_option("-a","--ambience",
                      type="float",default=0.2,dest="ambience",
                      help="ambient/diffuse light (0.0 to 1.0, default 0.2)")
    parser.add_option("-s","--specularity",
                      type="float",default=0.0,dest="specularity",
                      help="specularity of light (0.0 to 1.0, default 0.0)")
    parser.add_option("-o","--opengl",
                      type="str",default=None,dest="opengl",
                      help="set OpenGL wrapper to use (pyglet,PyOpenGL)")
    parser.add_option("-i","--image",
                      type="str",default=None,dest="imageloader",
                      help="set image loader (pyglet,pygame,PIL,none)")
    parser.add_option("-t","--tight",
                      action="store_true",default=False,dest="tight",
                      help="tighten error checking")
    parser.add_option("-e","--exception",
                      action="store_false",default=True,dest="recover",
                      help="don't try to recover from errors; raise exception")

    options,args = parser.parse_args()

    if len(args) != 1:
        parser.error("there must be exactly one position argument")

    filename = args[0]

    # Set which normals smoothing operation to use

    if options.normals == "none":
        options.nfunc = util.calculate_normals_no_smoothing
    elif options.normals == "cross":
        options.nfunc = util.calculate_normals_by_cross_product
    elif options.normals == "angle":
        options.nfunc = util.calculate_normals_by_angle_subtended
    else:
        parser.error("the argument to -n must be one of (none,cross,angle)")

    # Figure out which GUI to use

    if options.gui == "any":
        guimods = (
            ('OpenGL.GLUT', "glut"),
            ('pyglet', "pyglet"),
            ('pygame', "pygame")
            )
        for mod,gui in guimods:
            try:
                __import__(mod)
            except ImportError:
                pass
            else:
                options.gui = gui
                break
        else:
            raise RuntimeError("no suitable GUI library found")

    # Set the OpenGL wrapper and image loader to use now that we know
    # what GUI we are using

    if options.opengl is not None:
        config.OPENGL_PACKAGE = options.opengl
    elif options.gui == "pyglet":
        config.OPENGL_PACKAGE = "pyglet"

    if options.imageloader is not None:
        config.IMAGE_LOAD_PACKAGE = options.imageloader
    elif options.gui == "pygame":
        config.IMAGE_LOAD_PACKAGE = "PIL_or_pygame"
    elif options.gui == "pyglet":
        config.IMAGE_LOAD_PACKAGE = "PIL_or_pyglet"

    # We can import Dice3DS example models now that we have set the
    # configuration

    from Dice3DS.example import anygl
    globals().update(anygl.__dict__)

    global glmodel, gltexture
    from Dice3DS.example import glmodel, gltexture

    # Start the gui

    if options.gui == "pyglet":
        run_pyglet(filename,options)
    elif options.gui == "pygame":
        run_pygame(filename,options)
    elif options.gui == "glut":
        run_glut(filename,options)
    else:
        parser.error("the argument to -g must be one of "
                     "(pyglet,pygame,glut,any)")


if __name__ == '__main__':
    main()
