#!/usr/bin/env python
import sys
import time
import random
import pprint

import Xlib
from Xlib.display import Display
from gi.repository import Gtk, Gdk, GdkX11, GLib, GObject
from ctypes import *
from numpy import array

from OpenGL.GL import *
from OpenGL.GL import shaders
from OpenGL.GLU import gluPerspective, gluLookAt
from OpenGL.arrays import vbo
from OpenGL import GLX
try:
    from OpenGL.GLX import struct__XDisplay
except ImportError as err:
    from OpenGL.raw._GLX import struct__XDisplay
from OpenGL.GL import GL_VERTEX_SHADER, GL_FRAGMENT_SHADER
from OpenGL.GL import shaders, glGetUniformLocation

from helper import shader
from helper import cube as createcube

class gtkgl:
    """ these method do not seem to exist in python x11 library lets exploit the c methods 
    useful link http://www.opengl.org/wiki/Programming_OpenGL_in_Linux:_GLX_and_Xlib"""
    xlib = cdll.LoadLibrary('libX11.so')
    xlib.XOpenDisplay.argtypes = [c_char_p]
    xlib.XOpenDisplay.restype = POINTER(struct__XDisplay)
    xdisplay = xlib.XOpenDisplay(None)
    display = Xlib.display.Display()
    attrs = []

    xwindow_id = None
    width = height = 500

    def __init__(self):
        """ lets setup are opengl settings and create the context for our window """
        self.add_attribute(GLX.GLX_RGBA, True)
        self.add_attribute(GLX.GLX_RED_SIZE, 8)
        self.add_attribute(GLX.GLX_GREEN_SIZE, 8)
        self.add_attribute(GLX.GLX_BLUE_SIZE, 8)
        self.add_attribute(GLX.GLX_DOUBLEBUFFER, 1)
        self.add_attribute(GLX.GLX_DEPTH_SIZE, 24)

        xvinfo = GLX.glXChooseVisual(self.xdisplay, self.display.get_default_screen(), self.get_attributes())
        print("run glxinfo to match this visual id %s " % hex(xvinfo.contents.visualid))
        self.context = GLX.glXCreateContext(self.xdisplay, xvinfo, None, True)

    def add_attribute(self, setting, value):
        """just to nicely add opengl parameters"""
        self.attrs.append(setting)
        self.attrs.append(value)

    def get_attributes(self):
        """ return our parameters in the expected structure"""
        attrs = self.attrs + [0, 0]
        return (c_int * len(attrs))(*attrs)

    def configure(self, wid):
        """  """
        self.xwindow_id = GdkX11.X11Window.get_xid(wid)
        if(not GLX.glXMakeCurrent(self.xdisplay, self.xwindow_id, self.context)):
            print ('failed configuring context')
        glViewport(0, 0, self.width, self.height)

    def draw_start(self):
        """make cairo context current for drawing"""
        if(not GLX.glXMakeCurrent(self.xdisplay, self.xwindow_id, self.context)):
            print ("failed to get the context for drawing")

    def draw_finish(self):
        """swap buffer when we have finished drawing"""
        GLX.glXSwapBuffers(self.xdisplay, self.xwindow_id)

class scene:
    width, height = 600, 600

    rotationx = 0.0
    rotationy = 0.0

    rotation_incx = 0.5
    rotation_incy = 0.5
    radius = 0

    touch_count = 0
    touch_previous = 0, 0
    touch_start_one = 0, 0
    touch_start_two = 0, 0
    touch_end_one = 0, 0
    touch_end_two = 0, 0
    touch_time = 0

    camera_distance = 25

    cube_length = 1.0
    cube_size = cube_length / 2

    def __init__(self):
        """setup everything in the correct order"""
        self.glwrap = gtkgl()
        self.setup_opengl()
        self.generate()
        self.gui()

        self.mode = 'vbo all cubes'

    def gui(self):
        """load in the gui and connect the events and set our properties"""
        self.start_time = time.time()
        self.frame = 1
        xml = Gtk.Builder()
        xml.add_from_file('gui.glade')

        self.window = xml.get_object('window1')

        self.mode_widget = xml.get_object('cmbmode')
        self.mode_widget.connect('changed', self.change_mode)

        self.rotate_widget = xml.get_object('spinrotate')
        self.rotate_widget.connect('value-changed', self.change_rotate_speed)

        self.radius_widget = xml.get_object('spinradius')
        self.radius_widget.connect('value-changed', self.change_radius)

        self.color_widget = xml.get_object('btncolor')
        self.color_widget.connect('clicked', self.change_color)

        self.canvas_widget = xml.get_object('canvas')
        self.canvas_widget.connect('configure_event', self.on_configure_event)
        self.canvas_widget.connect('draw', self.on_draw)
        self.canvas_widget.set_double_buffered(False)
        self.canvas_widget.set_size_request(self.glwrap.width, self.glwrap.height)
        self.canvas_widget.add_events(Gdk.EventMask.TOUCH_MASK)
        self.canvas_widget.connect('touch-event', self.touched)

        self.window.show_all()
        GObject.idle_add(self.loop_draw)

    def touched(self, widget, ev):
        """basic touch support, count the touches so we no how many fingers
        basic pinc zoom along the x, single finger slide to rotate"""
        if ev.get_source_device().get_source() == Gdk.InputSource.TOUCHSCREEN:
            if ev.touch.type == Gdk.EventType.TOUCH_BEGIN:
                self.touch_start = ev.touch.x, ev.touch.y
                self.touch_count += 1
                if self.touch_count == 2:
                    self.touch_start_two = ev.touch.x, ev.touch.y
                    self.touch_previous =  ev.touch.x, ev.touch.y

        if ev.touch.type == Gdk.EventType.TOUCH_UPDATE:
            if ev.touch.time - self.touch_time < 100:
                return True
               
            if self.touch_count == 2:
                #basic pinch zoom along the x axis
                d1 = self.touch_previous[0] - ev.touch.x
                if d1 > 1:
                    self.camera_distance += self.camera_distance * 0.05
                    self.touch_previous =  ev.touch.x, ev.touch.y
                if d1 < 1:
                    self.camera_distance -= self.camera_distance * 0.05
                    self.touch_previous =  ev.touch.x, ev.touch.y
                self.update_camera()
            self.touch_time = ev.touch.time

        if ev.touch.type == Gdk.EventType.TOUCH_END:
            self.touch_end = ev.touch.x, ev.touch.y
            #set rotation when touch ends
            if self.touch_count == 1:
                self.rotation_incx = (self.touch_start[0] - self.touch_end[0]) * 0.01
                self.rotation_incy = (self.touch_start[1] - self.touch_end[1]) * 0.01
            self.touch_count = 0

    def in_circle(self, center_x, center_y, center_z, radius, x, y, z):
        """ test if our cordinate lies inside our sphere"""
        square_dist = (center_x - x) ** 2 + (center_y - y) ** 2 + (center_z - z) ** 2
        return square_dist <= radius ** 2

    def change_color(self, widget):
        #regenerate the scene
        self.generate()

    def change_mode(self, widget):
        #change whats drawn and how
        self.mode = widget.get_active_text().lower()
        print(widget.get_active_text())

    def change_rotate_speed(self, widget):
        #handle spinner rotation speed event
        self.rotation_incx = widget.get_value()
        self.rotation_incy = widget.get_value()
    
    def change_radius(self, widget):
        #increase size of circle and number of polygons
        self.radius = int(widget.get_value())
        self.generate()

    def loop_draw(self):
        #send redraw event to drawing area widget
        self.canvas_widget.queue_draw()
        return True

    def on_configure_event(self, widget, event):
        """if we recieve a configure event for example a resize, then grab the context settings and resize our scene """
        self.glwrap.width = widget.get_allocation().width
        self.glwrap.height = widget.get_allocation().height
        self.width, self.height = self.glwrap.width, self.glwrap.height

        #update our states because we have reconfigured the display
        self.glwrap.configure(widget.get_window())
        self.glwrap.draw_start()
        self.update_camera()
        self.setup_shaders()

        glEnable(GL_DEPTH_TEST)
        glDepthMask(GL_TRUE)
        glDepthFunc(GL_LEQUAL)
        glDepthRange(0.0, 1.0)
        glEnable(GL_CULL_FACE)
        glCullFace(GL_BACK)
        glFrontFace(GL_CW)

        self.glwrap.draw_finish()
        return True

    def on_draw(self, widget, context):
        """if we recieve a draw event redraw our opengl scene"""
        self.elapsed_time = time.time() - self.start_time
        self.frame += 1

        if self.elapsed_time > 1:
            print('fps %d' % self.frame)
            self.start_time = time.time()
            self.frame = 1
        self.glwrap.draw_start()
        self.draw()
        self.glwrap.draw_finish()

    def generate(self):
        self.cubes = []
        #position cubes inside a sphere radius
        for shift_x in range(-self.radius, self.radius + 1):
            for shift_y in range(-self.radius, self.radius + 1):
                for shift_z in range(-self.radius, self.radius + 1):
                    x = shift_x * self.cube_length
                    y = shift_y * self.cube_length
                    z = shift_z * self.cube_length
                    if not self.in_circle(0, 0, 0, self.radius, x, y, z):
                        continue
                    #random colours / textures if we want
                    color = random.choice([0.85, 0.15]), random.choice([0.85, 0.15]), random.choice([0.85, 0.15])
                    self.cubes.append(createcube((x, y, z), color, self.cube_size))

        self.test_cube = createcube((x, y, z), (random.choice([0.85, 0.15]), random.choice([0.85, 0.15]), random.choice([0.85, 0.15])), 6)
        faces = []
        for cube in self.cubes:
            faces += cube.get_data()

        print('Generated %s Cubes' % str(len(self.cubes)))
        print('Generated %s Tringles' % str(len(faces) / 3))
        self.vbuffer = vbo.VBO(array(faces, 'f'))

    def setup_shaders(self):
        self.shader_program = shader()
        self.shader_program.compile()

    def setup_opengl(self):
        glShadeModel(GL_SMOOTH)
        glClearColor(0.0, 0.0, 0.0, 0.0)
        glClearDepth(1.0)
        glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
        glPolygonMode(GL_FRONT, GL_FILL)

    def update_camera(self):
        glViewport(0, 0, self.width, self.height)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(45, 1.0 * self.width / self.height, 1.0, 80.0)
        gluLookAt(self.camera_distance, self.camera_distance, self.camera_distance,     # location
                  0.0, 0.0, 0.0,        # lookat
                  0.0, 1.0, 0.0)        # up direction
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()

    def draw_test(self):
        #lets do a simple rotation so we can see the objects are 3d
        glRotatef(self.rotationx, 1.0, 0.0, 0.0)
        self.rotationx += self.rotation_incx
        glRotatef(self.rotationy, 0.0, 1.0, 0.0)
        self.rotationy += self.rotation_incy

        #use our shader program and enable vertex loading 
        glUseProgram(self.shader_program.program)
        glEnableClientState(GL_VERTEX_ARRAY)
        glEnableClientState(GL_COLOR_ARRAY)

        #render the triangles into a  virtual buffer object
        self.test_cube.bind()
        glVertexPointer(3, GL_FLOAT, 24, self.test_cube.vbuffer)
        glColorPointer(3, GL_FLOAT, 24, self.test_cube.vbuffer + 12)
        glDrawArrays(GL_TRIANGLES, 0, self.test_cube.vbuffer_size)
        self.test_cube.unbind()

        #restore opengl to our previous state
        glDisableClientState(GL_COLOR_ARRAY)
        glDisableClientState(GL_VERTEX_ARRAY)
        shaders.glUseProgram(0)

    def draw_vbo_per_cube(self):
        #lets do a simple rotation so we can see the objects are 3d
        glRotatef(self.rotationx, 1.0, 0.0, 0.0)
        self.rotationx += self.rotation_incx
        glRotatef(self.rotationy, 0.0, 1.0, 0.0)
        self.rotationy += self.rotation_incy

        #  use our shader program and enable vertex loading 
        glUseProgram(self.shader_program.program)
        glEnableClientState(GL_VERTEX_ARRAY)
        glEnableClientState(GL_COLOR_ARRAY)

        #  render the triangles into a  virtual buffer object
        for shape in self.cubes:
            shape.bind()
            glVertexPointer(3, GL_FLOAT, 24, shape.vbuffer)
            glColorPointer(3, GL_FLOAT, 24, shape.vbuffer + 12)
            glDrawArrays(GL_TRIANGLES, 0, shape.vbuffer_size)
            shape.unbind()
 
        #restore opengl to our previous state
        glDisableClientState(GL_COLOR_ARRAY)
        glDisableClientState(GL_VERTEX_ARRAY)
        shaders.glUseProgram(0)

    def draw_vbo_all_cubes(self):
        #lets do a simple rotation so we can see the objects are 3d
        glRotatef(self.rotationx, 1.0, 0.0, 0.0)
        self.rotationx += self.rotation_incx
        glRotatef(self.rotationy, 0.0, 1.0, 0.0)
        self.rotationy += self.rotation_incy

        #  use our shader program and enable vertex loading 
        glUseProgram(self.shader_program.program)
        glEnableClientState(GL_VERTEX_ARRAY)
        glEnableClientState(GL_COLOR_ARRAY)

        #  render the triangles into a  virtual buffer object
        self.vbuffer.bind()
        glVertexPointer(3, GL_FLOAT, 24, self.vbuffer)
        glColorPointer(3, GL_FLOAT, 24, self.vbuffer + 12)
        glDrawArrays(GL_TRIANGLES, 0, len(self.vbuffer))
        self.vbuffer.unbind()

        #restore opengl to our previous state
        glDisableClientState(GL_COLOR_ARRAY)
        glDisableClientState(GL_VERTEX_ARRAY)
        shaders.glUseProgram(0)

    def draw(self):
        glEnable(GL_DEPTH_TEST)
        glClearDepth(1.0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glLoadIdentity()
        if self.mode == 'vbo test cube':
            self.draw_test()
        if self.mode == 'vbo per cube':
            self.draw_vbo_per_cube()
        if self.mode == 'vbo all cubes':
            self.draw_vbo_all_cubes()


if __name__ == '__main__': 
    glexample = scene()    
    GLib.threads_init()
    Gdk.threads_init()
    Gdk.threads_enter()
    Gtk.main()
    Gdk.threads_leave()
