"""
Description
-----------
ccad viewer designed to be imported from a python prompt or program.
View README for a full description of ccad.

display.py contains classes and functions for displaying.

The viewer uses python-qt4.

This version runs a low-level viewer for Linux and pythonocc's own
Display3d for other platforms.  Display3d has an error with multiple
viewing windows:

RuntimeError: Aspect_GraphicDeviceDefinitionError
Cannot connect to server

Author
------
View AUTHORS.

License
-------
Distributed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
View LICENSE for details.
"""

from __future__ import print_function
# Globals
version = '0.13'  # Change also in setup.py, doc/conf.py
interactive = True
manager = 'qt'
app = None


import os as _os
import sys as _sys
import math as _math

try:
    from PyQt4 import QtCore as _QtCore, QtGui as _QtGui
except ImportError:
    try:
        from PySide import QtCore as _QtCore, QtGui as _QtGui
    except:
        manager = 'none'
        print("""
Warning: Cannot find python-qt4 or pyside.  You will not be able to
use ccad's display.  Instead, you may use pythonocc's viewers.  ccad
shapes may be displayed in pythonocc's viewers by using the .shape
attribute.  For example:

    import ccad.model as cm
    import OCC.Display.SimpleGui as SimpleGui

    s1 = cm.sphere(1.0)
    display, start_display, add_menu, add_function_to_menu = \
        SimpleGui.init_display()
    display.DisplayShape(s1.shape, update = True)
    start_display()
""")

from OCC import (AIS as _AIS, Aspect as _Aspect, gp as _gp,
                 Graphic3d as _Graphic3d, Prs3d as _Prs3d,
                 Quantity as _Quantity, TopAbs as _TopAbs, V3d as _V3d)
from OCC.BRepTools import BRepTools_WireExplorer as _BRepTools_WireExplorer
from OCC.HLRAlgo import HLRAlgo_Projector as _HLRAlgo_Projector
from OCC.HLRBRep import (HLRBRep_Algo as _HLRBRep_Algo,
                         HLRBRep_HLRToShape as _HLRBRep_HLRToShape)
from OCC.TCollection import (TCollection_ExtendedString as
                             _TCollection_ExtendedString)
from OCC.TopExp import TopExp_Explorer as _TopExp_Explorer
from OCC.Visual3d import Visual3d_ViewOrientation as _Visual3d_ViewOrientation
from OCC.Visualization import Display3d as _Display3d

import ccad.model as _cm


class view_qt(_QtGui.QWidget):
    """
    A qt-based viewer
    """

    def __init__(self, perspective=False):
        """
        Perspective doesn't seem to work in pythonocc ***.  Don't use.
        """
        super(view_qt, self).__init__()
        self.setMouseTracking(True)
        #self.setFocusPolicy(_QtCore.Qt.WheelFocus)
        #self.setSizePolicy(_QtGui.QSizePolicy(_QtGui.QSizePolicy.Ignored,
        #                                      _QtGui.QSizePolicy.Ignored))

        self.REGULAR_CURSOR = _QtCore.Qt.ArrowCursor
        self.WAIT_CURSOR = _QtCore.Qt.WaitCursor

        self.key_table = {'redraw()': 'PgUp',
                          'orbitup()': 'Up',
                          'panup()': '8',
                          'orbitdown()': 'Down',
                          'pandown()': '2',
                          'orbitleft()': 'Left',
                          'panleft()': '4',
                          'orbitright()': 'Right',
                          'panright()': '6',
                          'rotateccw()': '/',
                          'rotatecw()': '*',
                          'zoomin()': '+',
                          'zoomout()': '-',
                          'fit()': 'Del',
                          'query()': 'Q',
                          'viewstandard("top")': 'Home',
                          'viewstandard("bottom")': '7',
                          'viewstandard("front")': 'End',
                          'viewstandard("back")': '1',
                          'viewstandard("right")': 'PgDown',
                          'viewstandard("left")': '3',
                          'quit()': 'Ctrl+Q'}

        self.selected = None
        self.selected_shape = None
        self.selection_type = 'shape'
        self.selection_index = -1
        self.foreground = (1.0, 1.0, 0.0)  # Bright-Yellow is default
        self.display_shapes = []

        # Main Window
        self.setWindowTitle('ccad viewer')
        self.menus = {}

        # Vertical Container
        vbox1 = _QtGui.QVBoxLayout()
        vbox1.setMargin(0)
        vbox1.setSpacing(0)

        ## Menu Space
        self.menubar = _QtGui.QMenuBar()
        vbox1.setMenuBar(self.menubar)

        ### File
        file_menu = _QtGui.QMenu('&File', self)
        self.menubar.addMenu(file_menu)

        file_save = file_menu.addAction('&Save', self.save)
        file_quit = file_menu.addAction('&Quit', self.quit,
                                        self.key_lookup('quit()'))

        ### view
        view_menu = _QtGui.QMenu('&View', self)
        self.menubar.addMenu(view_menu)

        view_mode = _QtGui.QMenu('Mode', self)
        view_menu.addMenu(view_mode)
        view_mode_container = _QtGui.QActionGroup(self)
        view_mode_wireframe = view_mode.addAction('Wireframe',
                                                  self.mode_wireframe)
        view_mode_wireframe.setCheckable(True)
        view_mode_container.addAction(view_mode_wireframe)
        view_mode_shaded = view_mode.addAction('Shaded', self.mode_shaded)
        view_mode_shaded.setCheckable(True)
        view_mode_shaded.setChecked(True)
        view_mode_container.addAction(view_mode_shaded)
        view_mode_hlr = view_mode.addAction('Hidden Line Removal',
                                            self.mode_hlr)
        view_mode_hlr.setCheckable(True)
        view_mode_container.addAction(view_mode_hlr)

        view_side = _QtGui.QMenu('Side', self)
        view_menu.addMenu(view_side)
        view_front = view_side.addAction(
            'Front',
            lambda view='front': self.viewstandard(view),
            self.key_lookup('viewstandard("front")'))
        view_top = view_side.addAction(
            'Top',
            lambda view='top': self.viewstandard(view),
            self.key_lookup('viewstandard("top")'))
        view_right = view_side.addAction(
            'Right',
            lambda view='right': self.viewstandard(view),
            self.key_lookup('viewstandard("right")'))
        view_back = view_side.addAction(
            'Back',
            lambda view='back': self.viewstandard(view),
            self.key_lookup('viewstandard("back")'))
        view_bottom = view_side.addAction(
            'Bottom',
            lambda view='bottom': self.viewstandard(view),
            self.key_lookup('viewstandard("bottom")'))
        view_left = view_side.addAction(
            'Left',
            lambda view='left': self.viewstandard(view),
            self.key_lookup('viewstandard("left")'))

        view_orbit = _QtGui.QMenu('Orbit', self)
        view_menu.addMenu(view_orbit)
        view_orbitup = view_orbit.addAction('Up', self.orbitup,
                                            self.key_lookup('orbitup()'))
        view_orbitdown = view_orbit.addAction('Down', self.orbitdown,
                                              self.key_lookup('orbitdown()'))
        view_orbitleft = view_orbit.addAction('Left', self.orbitleft,
                                              self.key_lookup('orbitleft()'))
        view_orbitright = view_orbit.addAction('Right', self.orbitright,
                                               self.key_lookup('orbitright()'))
        view_orbitccw = view_orbit.addAction('CCW', self.rotateccw,
                                             self.key_lookup('rotateccw()'))
        view_orbitleft = view_orbit.addAction('CW', self.rotatecw,
                                              self.key_lookup('rotatecw()'))

        view_pan = _QtGui.QMenu('Pan', self)
        view_menu.addMenu(view_pan)
        view_panup = view_pan.addAction('Up', self.panup,
                                        self.key_lookup('panup()'))
        view_pandown = view_pan.addAction('Down', self.pandown,
                                          self.key_lookup('pandown()'))
        view_panleft = view_pan.addAction('Left', self.panleft,
                                          self.key_lookup('panleft()'))
        view_panright = view_pan.addAction('Right', self.panright,
                                           self.key_lookup('panright()'))

        view_zoom = _QtGui.QMenu('Zoom', self)
        view_menu.addMenu(view_zoom)
        view_zoomin = view_zoom.addAction('In', self.zoomin,
                                          self.key_lookup('zoomin()'))
        view_zoomout = view_zoom.addAction('Out', self.zoomout,
                                           self.key_lookup('zoomout()'))
        view_fit = view_zoom.addAction('Fit to Screen', self.fit,
                                       self.key_lookup('fit()'))

        view_menu.addAction('Redraw', self.redraw, self.key_lookup('redraw()'))

        ### select
        select_menu = _QtGui.QMenu('&Select', self)
        self.menubar.addMenu(select_menu)
        select_container = _QtGui.QActionGroup(self)

        select_vertex = select_menu.addAction('Select Vertex',
                                              self.select_vertex)
        select_vertex.setCheckable(True)
        select_container.addAction(select_vertex)

        select_edge = select_menu.addAction('Select Edge', self.select_edge)
        select_edge.setCheckable(True)
        select_container.addAction(select_edge)

        select_wire = select_menu.addAction('Select Wire', self.select_wire)
        select_wire.setCheckable(True)
        select_container.addAction(select_wire)

        select_face = select_menu.addAction('Select Face', self.select_face)
        select_face.setCheckable(True)
        select_container.addAction(select_face)

        select_shape = select_menu.addAction('Select Shape', self.select_shape)
        select_shape.setCheckable(True)
        select_shape.setChecked(True)
        select_container.addAction(select_shape)

        select_query = select_menu.addAction('Query',
                                             self.query,
                                             self.key_lookup('query()'))

        ### help
        help_menu = _QtGui.QMenu('&Help', self)
        self.menubar.addMenu(help_menu)
        help_manual = help_menu.addAction('&Manual', self.display_manual)
        help_about = help_menu.addAction('&About', self.about)

        # OpenGL Space
        self.glarea = GLWidget(self)
        vbox1.addWidget(self.glarea)

        # Status Line
        self.status_bar = _QtGui.QLabel()
        self.status_bar.setText('')
        vbox1.addWidget(self.status_bar)

        self.setLayout(vbox1)
        self.show()

        self.glarea.start(perspective)

        # Some Initial Values
        self.mode_shaded()
        self.set_background((0.0, 0.0, 0.0))
        self.set_triedron(1)

        # Set up some initial states
        self.morbit = _math.pi / 12.0
        self.set_scale(10.0)

    def add_menu(self, hierarchy):
        """
        Add a menu.  Used internally.  Used externally for those who
        know the window manager well, and want to add fancy menu items
        (radio buttons, check boxes, items with graphics, etc.)
        manually.  For text-only menu items, use add_menuitem.
        """
        last_menu = self.menubar
        for sub_menu in hierarchy:
            if sub_menu not in self.menus:
                menu = _QtGui.QMenu(sub_menu, self)
                last_menu.addMenu(menu)
                self.menus[sub_menu] = menu
            last_menu = self.menus[sub_menu]
        return last_menu

    def add_menuitem(self, hierarchy, func, *args):
        """
        Add a menu item.  hierarchy is a tuple.  The first element in
        the tuple is the main menu at the menubar level, and the last
        item is the menu item.  Intermediate items may be specified,
        if there are submenus.  func is the function to call when the
        menu is selected.  args are any pass parameters to pass to the
        function.

        Generates all needed menus to create the passed hierarchy.
        """
        last_menu = self.add_menu(hierarchy[:-1])
        menuitem = last_menu.addAction(hierarchy[-1], lambda: func(*args))

    # Event Functions
    def redraw(self):
        """
        Called from a user request
        """
        self.glarea.paintEvent(None)

    def key_lookup(self, func_call):
        """
        Connects a key press to a function in the key_table
        """
        key = self.key_table[func_call]
        return key

    def keyPressEvent(self, event):
        """
        Called when a key is pressed
        """
        key = event.key()
        self.status_bar.setText('Key ' + hex(key))
        if key in self.key_table.values():
            try:
                cmd = self.key_table.keys()[self.key_table.values().index(key)]
                eval('self.' + cmd)
            except:
                self.status_bar.setText('Command unknown ' + cmd)

    def mousePressEvent(self, event):
        """
        Called when a mouse button is pressed
        """
        pos = self.glarea.mapFromParent(event.pos())
        self.beginx, self.beginy = pos.x(), pos.y()
        self.glarea.occ_view.StartRotation(self.beginx, self.beginy)

    def mouseReleaseEvent(self, event):
        """
        Called when a mouse button is released
        """
        if event.button() == _QtCore.Qt.RightButton:  # Selection
            self.glarea.occ_context.Select()
            self.glarea.occ_context.InitSelected()
            if self.glarea.occ_context.MoreSelected():
                if self.glarea.occ_context.HasSelectedShape():
                    self.selected = self.glarea.occ_context.SelectedShape()
            else:
                self.selected = None
            self.make_selection()

    def mouseMoveEvent(self, event):
        """
        Called when a mouse button is pressed and the mouse is moving
        """
        pos = self.glarea.mapFromParent(event.pos())
        x, y = pos.x(), pos.y()
        # Mouse-Controlled Projection
        # ComputedModes are too slow to redraw, so disabled for them
        if (event.buttons() & _QtCore.Qt.MidButton) and \
                not self.glarea.occ_view.ComputedMode():
            # Mouse-Controlled Pan
            if event.modifiers() & _QtCore.Qt.ShiftModifier:
                self.glarea.occ_view.Pan(x - self.beginx, -y + self.beginy)
                self.beginx, self.beginy = x, y
            else:  # Mouse-Controlled Orbit
                self.glarea.occ_view.Rotation(x, y)
        self.glarea.occ_context.MoveTo(x, y, self.glarea.handle_view)

    # View Functions
    def viewstandard(self, viewtype='front'):
        """
        Sets up the viewing projection according to a standard set of views
        """
        if viewtype == 'front':
            self.glarea.occ_view.SetProj(_V3d.V3d_Yneg)
        elif viewtype == 'back':
            self.glarea.occ_view.SetProj(_V3d.V3d_Ypos)
        elif viewtype == 'top':
            self.glarea.occ_view.SetProj(_V3d.V3d_Zpos)
        elif viewtype == 'bottom':
            self.glarea.occ_view.SetProj(_V3d.V3d_Zneg)
        elif viewtype == 'right':
            self.glarea.occ_view.SetProj(_V3d.V3d_Xpos)
        elif viewtype == 'left':
            self.glarea.occ_view.SetProj(_V3d.V3d_Xneg)
        elif viewtype == 'iso':
            self.glarea.occ_view.SetProj(_V3d.V3d_XposYnegZpos)
        elif viewtype == 'iso_back':
            self.glarea.occ_view.SetProj(_V3d.V3d_XnegYposZneg)
        else:
            self.status_bar.setText('Unknown view' + viewtype)

    def orbitup(self, widget=None, rapid=False):
        """
        The observer has moved up

        All orbits orbit with respect to (0,0,0).  That means points
        far from (0,0,0) will translate as you orbit.  I'd prefer it
        orbiting with respect to the center of the screen.  That
        should be possible using the Gravity method from occ_view, but
        pythonocc doesn't implement OCC's Gravity.
        """
        # The better way (pythonocc doesn't implement)
        #gravity = self.glarea.occ_view.Gravity()
        #self.glarea.occ_view.Rotate(0.0, -self.morbit, 0.0,
        #                            gravity[0], gravity[1], gravity[2])
        self.glarea.occ_view.Rotate(0.0, -self.morbit, 0.0)

    def panup(self, widget=None, rapid=False):
        """
        The scene is panned up
        """
        self.glarea.occ_view.Pan(0, -self.glarea.mpan)

    def orbitdown(self, widget=None, rapid=False):
        """
        The observer has moved down
        """
        self.glarea.occ_view.Rotate(0.0, self.morbit, 0.0)

    def pandown(self, widget=None, rapid=False):
        """
        The scene is panned down
        """
        self.glarea.occ_view.Pan(0, self.glarea.mpan)

    def orbitright(self, widget=None, rapid=False):
        """
        The observer has moved to the right
        """
        self.glarea.occ_view.Rotate(-self.morbit, 0.0, 0.0)

    def panright(self, widget=None, rapid=False):
        """
        The scene is panned right
        """
        self.glarea.occ_view.Pan(-self.glarea.mpan, 0)

    def orbitleft(self, widget=None, rapid=False):
        """
        The observer has moved to the left
        """
        self.glarea.occ_view.Rotate(self.morbit, 0.0, 0.0)

    def panleft(self, widget=None, rapid=False):
        """
        The scene is panned to the left
        """
        self.glarea.occ_view.Pan(self.glarea.mpan, 0)

    def zoomin(self, widget=None, rapid=False):
        """
        Zoom in
        """
        self.glarea.occ_view.SetZoom(_math.sqrt(2.0))

    def zoomout(self, widget=None, rapid=False):
        """
        Zoom out
        """
        self.glarea.occ_view.SetZoom(_math.sqrt(0.5))

    def rotateccw(self, widget=None, rapid=False):
        """
        The scene is rotated counter clockwise
        """
        self.glarea.occ_view.Rotate(0.0, 0.0, -self.morbit)

    def rotatecw(self, widget=None, rapid=False):
        """
        The scene is rotated clockwise
        """
        self.glarea.occ_view.Rotate(0.0, 0.0, self.morbit)

    def fit(self, widget=None):
        """
        Fit the scene to the screen
        """
        self.glarea.occ_view.ZFitAll()
        self.glarea.occ_view.FitAll()

    def query(self, widget=None):
        """
        Reports the properties of a selection
        Should do something other than print (popup?) ***
        """
        if self.selected is not None:
            if self.selection_type == 'vertex':
                s = _cm.vertex(self.selected)
                retval = 'center: ' + str(s.center()) + \
                    '\ntolerance: ' + str(s.tolerance())
            elif self.selection_type == 'edge':
                s = _cm.edge(self.selected)
                retval = 'center: ' + str(s.center()) + \
                    '\nlength: ' + str(s.length()) + \
                    '\ntolerance: ' + str(s.tolerance())
            elif self.selection_type == 'wire':
                s = _cm.wire(self.selected)
                retval = 'center: ' + str(s.center()) + \
                    '\nlength: ' + str(s.length())
            elif self.selection_type == 'face':
                s = _cm.face(self.selected)
                retval = 'center: ' + str(s.center()) + \
                    '\ntype: ' + str(s.type()) + \
                    '\narea: ' + str(s.area()) + \
                    '\ntolerance: ' + str(s.tolerance())
            else:
                retval = 'No properties for type ' + self.selection_type
            print(retval)

    # Direct Call (not from GUI) Functions
    def set_projection(self, vcenter, vout, vup):
        """
        Set the projection to a custom view given

        vcenter, the scene coordinates in the center of the window,
        vout, the vector from vcenter in scene coordinates out of the window,
        vup, the vector from vcenter in scene coordinates that show straight up
        """

        projection = _Visual3d_ViewOrientation(
            _Graphic3d.Graphic3d_Vertex(vcenter[0], vcenter[1], vcenter[2]),
            _Graphic3d.Graphic3d_Vector(vout[0], vout[1], vout[2]),
            _Graphic3d.Graphic3d_Vector(vup[0], vup[1], vup[2]))
        self.glarea.occ_view.SetViewOrientation(projection)

    def set_scale(self, scale):
        """
        Set the screen scale.  I'm not certain, but it looks to me
        like scale is the number of scene-coordinates in the
        x-direction.  For example, if you have a block 8.0 wide in the
        x-direction, and you set the scale to 8.0, the block will
        exactly fill the screen in the x-direction.
        """
        self.glarea.occ_view.SetSize(scale)

    def set_size(self, size):
        """
        Sets the size of the window in pixels.  Size is a 2-tuple.
        """
        # This worked, but adjustSize is limited to 2/3 screen size
        #self.glarea.SCR = size
        #self.glarea.updateGeometry()
        #self.adjustSize()

        # self.glarea.resize didn't work.  This worked but it's based
        # on the non-glarea stuff not growing -- a potential future
        # bug.
        all_size = self.size()
        glarea_size = self.glarea.size()
        dx = all_size.width() - glarea_size.width()
        dy = all_size.height() - glarea_size.height()
        self.resize(size[0] + dx, size[1] + dy)

    def set_background(self, color):
        """
        Sets the background color.
        color is a 3-tuple with each value from 0.0 to 1.0
        """
        self.glarea.occ_view.SetBackgroundColor(
            _Quantity.Quantity_TOC_RGB, color[0], color[1], color[2])

    def set_foreground(self, color):
        """
        Sets the default shape color.
        color is a 3-tuple with each value from 0.0 to 1.0
        """
        self.foreground = color

    def set_triedron(self, state, position='bottom_right',
                     color=(1.0, 1.0, 1.0), size=0.08):
        """
        Controls the triedron, the little x, y, z coordinate display.

        state (1 or 0) turns it on or off
        position sets the position of the triedron in the window.
        color sets the triedron color (only black or white, currently)
        size sets the triedron size in scene-coordinates
        """
        if not state:
            self.glarea.occ_view.TriedronErase()
        else:
            local_positions = {'bottom_right': _Aspect.Aspect_TOTP_RIGHT_LOWER,
                               'bottom_left': _Aspect.Aspect_TOTP_LEFT_LOWER,
                               'top_right': _Aspect.Aspect_TOTP_RIGHT_UPPER,
                               'top_left': _Aspect.Aspect_TOTP_LEFT_UPPER}
            local_position = local_positions[position]
            # Can't set Triedron color RGB-wise!
            #qcolor = _Quantity.Quantity_Color(
            #    color[0], color[1], color[2], _Quantity.Quantity_TOC_RGB)
            if color == (1.0, 1.0, 1.0):
                qcolor = _Quantity.Quantity_NOC_WHITE
            else:
                qcolor = _Quantity.Quantity_NOC_BLACK
            self.glarea.occ_view.TriedronDisplay(local_position,
                                                 qcolor,
                                                 size,
                                                 _V3d.V3d_ZBUFFER)
                                                 #_V3d.V3d_WIREFRAME)

    # Things to Show Functions
    def display(self, shape, color=None, material='default', transparency=0.0,
                line_type='solid', line_width=1, logging=True):
        """
        Displays a ccad shape.

        color is used for all shape types.  It is a tuple of (R, G, B)
        from 0.0 to 1.0.

        material sets the solid material (unused for non-solids).
        Material can be:

        brass
        bronze
        copper
        gold
        pewter
        plaster
        plastic
        silver
        steel
        stone
        shiny_plastic
        satin
        metallized
        neon_gnc
        chrome
        aluminum
        obsidian
        neon_phc
        jade
        default

        transparency sets the solid transparency; 0 is opaque; 1 is
        transparent

        line_type can be solid, dash, or dot for edges and wires

        line_width sets the edge or wire width in pixels

        logging allows you to keep a list of all shapes displayed
        """
        if hasattr(shape, 'shape'):
            s = shape.shape
        else:
            s = shape
        self.selected_shape = s
        display_shape = {'shape': s,
                         'color': color,
                         'material': material,
                         'transparency': transparency,
                         'line_type': line_type,
                         'line_width': line_width}
        if logging:
            self.display_shapes.append(display_shape)
        aisshape = _AIS.AIS_Shape(s)
        handle_aisshape = aisshape.GetHandle()

        # Set Color
        if not color:
            color = self.foreground
        #print('color', color)

        #drawer = AIS_Drawer()
        #handle_drawer = drawer.GetHandle()

        handle_drawer = aisshape.Attributes()
        drawer = handle_drawer.GetObject()

        qcolor = _Quantity.Quantity_Color(color[0],
                                          color[1],
                                          color[2],
                                          _Quantity.Quantity_TOC_RGB)

        # Set Point Type
        aspect_point = _Prs3d.Prs3d_PointAspect(_Aspect.Aspect_TOM_PLUS,
                                                qcolor, 1.0)
        handle_aspect_point = aspect_point.GetHandle()
        drawer.SetPointAspect(handle_aspect_point)

        # Set Line Type
        local_line_type = {'solid': _Aspect.Aspect_TOL_SOLID,
                           'dash': _Aspect.Aspect_TOL_DASH,
                           'dot': _Aspect.Aspect_TOL_DOT}[line_type]
        aspect_line = _Prs3d.Prs3d_LineAspect(qcolor,
                                              local_line_type,
                                              line_width)
        handle_aspect_line = aspect_line.GetHandle()
        #drawer = self.glarea.occ_context.DefaultDrawer().GetObject()
        drawer.SetSeenLineAspect(handle_aspect_line)
        drawer.SetWireAspect(handle_aspect_line)

        # Set Shading Type
        aspect_shading = _Prs3d.Prs3d_ShadingAspect()
        handle_aspect_shading = aspect_shading.GetHandle()
        #print('shading color', color)
        aspect_shading.SetColor(qcolor, _Aspect.Aspect_TOFM_BOTH_SIDE)
        local_materials = {'brass': _Graphic3d.Graphic3d_NOM_BRASS,
                           'bronze': _Graphic3d.Graphic3d_NOM_BRONZE,
                           'copper': _Graphic3d.Graphic3d_NOM_COPPER,
                           'gold': _Graphic3d.Graphic3d_NOM_GOLD,
                           'pewter': _Graphic3d.Graphic3d_NOM_PEWTER,
                           'plaster': _Graphic3d.Graphic3d_NOM_PLASTER,
                           'plastic': _Graphic3d.Graphic3d_NOM_PLASTIC,
                           'silver': _Graphic3d.Graphic3d_NOM_SILVER,
                           'steel': _Graphic3d.Graphic3d_NOM_STEEL,
                           'stone': _Graphic3d.Graphic3d_NOM_STONE,
                           'shiny_plastic': \
                               _Graphic3d.Graphic3d_NOM_SHINY_PLASTIC,
                           'satin': _Graphic3d.Graphic3d_NOM_SATIN,
                           'metallized': _Graphic3d.Graphic3d_NOM_METALIZED,
                           'neon_gnc': _Graphic3d.Graphic3d_NOM_NEON_GNC,
                           'chrome': _Graphic3d.Graphic3d_NOM_CHROME,
                           'aluminum': _Graphic3d.Graphic3d_NOM_ALUMINIUM,
                           'obsidian': _Graphic3d.Graphic3d_NOM_OBSIDIAN,
                           'neon_phc': _Graphic3d.Graphic3d_NOM_NEON_PHC,
                           'jade': _Graphic3d.Graphic3d_NOM_JADE,
                           'default': _Graphic3d.Graphic3d_NOM_DEFAULT}
        local_material = local_materials[material]
        aspect_shading.SetMaterial(local_material)
        aspect_shading.SetTransparency(transparency)
        drawer.SetShadingAspect(handle_aspect_shading)

        self.glarea.occ_context.Display(handle_aisshape, True)

    def clear(self, display_shapes=True):
        """
        Clears all shapes from the window
        """
        self.select_shape()
        self.glarea.occ_context.PurgeDisplay()
        self.glarea.occ_context.EraseAll()
        if display_shapes:
            self.display_shapes = []

    # Selection Functions
    def _build_hashes(self, htype):
        if htype == 'face':
            ex_type = _TopAbs.TopAbs_FACE
        elif htype == 'wire':
            ex_type = _TopAbs.TopAbs_WIRE
        elif htype == 'edge':
            ex_type = _TopAbs.TopAbs_EDGE
        elif htype == 'vertex':
            ex_type = _TopAbs.TopAbs_VERTEX
        else:
            print('Error: Unknown hash type', htype)
        if (self.selected_shape.ShapeType == _TopAbs.TopAbs_WIRE and
            htype == 'edge'):
            ex = _BRepTools_WireExplorer(selected_shape)  # Ordered this way
        else:
            ex = _TopExp_Explorer(self.selected_shape, ex_type)
        self.hashes = []
        self.positions = []
        while ex.More():
            s1 = ex.Current()
            # Calculate hash
            s1_hash = s1.__hash__()
            if s1_hash not in self.hashes:
                self.hashes.append(s1_hash)
                # Calculate position
                if htype == 'face':
                    f = _cm.face(s1)
                    c = (' type ' + f.type(), f.center())
                elif htype == 'wire':
                    w = _cm.wire(s1)
                    c = ('', w.center())
                elif htype == 'edge':
                    e = _cm.edge(s1)
                    c = ('', e.center())
                elif htype == 'vertex':
                    c = ('', _cm.vertex(s1).center())
                self.positions.append(c)
            ex.Next()

    def make_selection(self, event=None):
        """
        Called when a shape is selected
        """
        if self.selected is not None:
            if self.selection_type == 'shape':
                self.selected_shape = self.selected
            else:
                h = self.selected.__hash__()
                try:
                    index = self.hashes.index(h)
                except ValueError:
                    index = -1
                if index == -1:
                    self.status_bar.setText('Select shape first.')
                else:
                    status = self.selection_type + ' ' + str(index) + \
                        self.positions[index][0] + \
                        ' at (%.9f, %.9f, %.9f)' % self.positions[index][1]
                    print(status)
                    self.status_bar.setText(status)
                self.selection_index = index

    def select_vertex(self, event=None):
        """
        Changes to a mode where only vertices can be selected
        """
        self.setCursor(self.WAIT_CURSOR)
        self.glarea.occ_context.CloseAllContexts()
        self.glarea.occ_context.OpenLocalContext()
        self.glarea.occ_context.ActivateStandardMode(_TopAbs.TopAbs_VERTEX)
        self._build_hashes('vertex')
        self.selection_type = 'vertex'
        self.setCursor(self.REGULAR_CURSOR)

    def select_edge(self, event=None):
        """
        Changes to a mode where only edges can be selected
        """
        self.setCursor(self.WAIT_CURSOR)
        self.glarea.occ_context.CloseAllContexts()
        self.glarea.occ_context.OpenLocalContext()
        self.glarea.occ_context.ActivateStandardMode(_TopAbs.TopAbs_EDGE)
        self._build_hashes('edge')
        self.selection_type = 'edge'
        self.setCursor(self.REGULAR_CURSOR)

    def select_wire(self, event=None):
        """
        Changes to a mode where only wires can be selected
        """
        self.setCursor(self.WAIT_CURSOR)
        self.glarea.occ_context.CloseAllContexts()
        self.glarea.occ_context.OpenLocalContext()
        self.glarea.occ_context.ActivateStandardMode(_TopAbs.TopAbs_WIRE)
        self._build_hashes('wire')
        self.selection_type = 'wire'
        self.setCursor(self.REGULAR_CURSOR)

    def select_face(self, event=None):
        """
        Changes to a mode where only faces can be selected
        """
        self.setCursor(self.WAIT_CURSOR)
        self.glarea.occ_context.CloseAllContexts()
        self.glarea.occ_context.OpenLocalContext()
        self.glarea.occ_context.ActivateStandardMode(_TopAbs.TopAbs_FACE)
        self._build_hashes('face')
        self.selection_type = 'face'
        self.setCursor(self.REGULAR_CURSOR)

    def select_shape(self, event=None):
        """
        Changes to a mode where only shapes can be selected
        """
        self.setCursor(self.WAIT_CURSOR)
        self.glarea.occ_context.CloseAllContexts()
        self.selection_type = 'shape'
        self.setCursor(self.REGULAR_CURSOR)

    # Viewing Mode Functions
    def mode_wireframe(self, widget=None):
        """
        Changes the display to view shapes as wireframes
        """
        if not widget or (widget and widget.get_active()):
            self.glarea.occ_view.SetComputedMode(False)
            self.glarea.occ_context.SetDisplayMode(_AIS.AIS_WireFrame)

    def mode_shaded(self, widget=None):
        """
        Changes the display to view shapes as shaded (filled) shapes.
        """
        if not widget or (widget and widget.get_active()):
            self.glarea.occ_view.SetComputedMode(False)
            self.glarea.occ_context.SetDisplayMode(_AIS.AIS_Shaded)

    def mode_hlr(self, widget=None):
        """
        Changes the display to view shapes in hidden line removal
        mode, where the part outline, sharp edges, and face barriers
        are shown as lines.
        """
        if not widget or (widget and widget.get_active()):
            self.glarea.occ_view.SetComputedMode(True)
            self.glarea.occ_context.SetDisplayMode(_AIS.AIS_ExactHLR)

        # Draws hidden lines
        #presentation = Prs3d_LineAspect(
        #    Quantity_NOC_BLACK, Aspect_TOL_DASH, 3)
        #self.glarea.occ_context.SetHiddenLineAspect(presentation.GetHandle())
        #self.glarea.occ_context.EnableDrawHiddenLine()

    def reset_mode_drawing(self):
        """
        Call this after mode_drawing to reset everything.
        """
        self.view_shaded.set_active(True)
        self.clear(0)
        self.glarea.occ_view.SetViewOrientation(self.saved_projection)
        for display_shape in self.display_shapes:
            self.display(display_shape['shape'],
                         display_shape['color'],
                         display_shape['material'],
                         display_shape['transparency'],
                         display_shape['line_type'],
                         display_shape['line_width'],
                         logging=0)

    def mode_drawing(self, widget=None):
        """
        This is a stand-alone call to make a drafting-like drawing of
        the shape.  It's better than HLR, because HLR shows creases at
        edges where shapes are tangent.  If this must be a menu call,
        pop up a separate window for it.
        """
        self.saved_projection = self.glarea.occ_view.ViewOrientation()
        # Graphic3d_Vertex
        vcenter = self.saved_projection.ViewReferencePoint()
        vout = self.saved_projection.ViewReferencePlane()  # Graphic3d_Vector
        vup = self.saved_projection.ViewReferenceUp()  # Graphic3d_Vector
        vout_gp = _gp.gp_Vec(vout.X(), vout.Y(), vout.Z())
        vright = _gp.gp_Vec(vup.X(), vup.Y(), vup.Z())
        vright.Cross(vout_gp)
        projection = _HLRAlgo_Projector(
            _gp.gp_Ax2(_gp.gp_Pnt(vcenter.X(), vcenter.Y(), vcenter.Z()),
                       _gp.gp_Dir(vout.X(), vout.Y(), vout.Z()),
                       _gp.gp_Dir(vright.X(), vright.Y(), vright.Z())))
        hlr_algo = _HLRBRep_Algo()
        handle_hlr_algo = hlr_algo.GetHandle()
        for display_shape in self.display_shapes:
            hlr_algo.Add(display_shape['shape'])
        hlr_algo.Projector(projection)
        hlr_algo.Update()
        hlr_algo.Hide()
        hlr_toshape = _HLRBRep_HLRToShape(handle_hlr_algo)
        vcompound = hlr_toshape.VCompound()
        outlinevcompound = hlr_toshape.OutLineVCompound()
        self.clear(0)
        self.display(vcompound,
                     color=display_shape['color'],
                     line_type=display_shape['line_type'],
                     line_width=display_shape['line_width'],
                     logging=False)
        self.display(outlinevcompound,
                     color=display_shape['color'],
                     line_type=display_shape['line_type'],
                     line_width=display_shape['line_width'],
                     logging=False)
        self.viewstandard(viewtype='top')

    # Helps
    def display_manual(self):
        """
        Displays the manual
        """

        # I couldn't get the directory to work for all platforms.
        if _sys.platform.startswith('linux'):
            updirs = 4
        elif _sys.platform.startswith('win'):
            updirs = 3
        elif _sys.platform.startswith('darwin'):
            updirs = 4  # Not debugged
        else:
            updirs = 0

        doc_directory = _os.path.normpath(
            _os.path.join(
                _os.path.dirname(__file__),
                '../' * updirs + 'share/doc/ccad/html'))
        fullname = _os.path.join(doc_directory, 'contents.html')

        if _os.path.exists(fullname):
            if _sys.platform.startswith('linux'):
                _os.system('xdg-open ' + fullname + ' &')
            elif _sys.platform.startswith('win'):
                _os.system('start ' + fullname)
            elif _sys.platform.startswith('darwin'):
                _os.system('open ' + fullname)
            else:
                self.status_bar.setText(
                    'Warning: viewer not found for ' + _sys.platform)
        else:
            self.status_bar.setText('Warning: cannot find ' + fullname)

    def about(self):
        """
        Pops up a window about ccad
        """
        global version
        _QtGui.QMessageBox.about(
            self,
            'ccad viewer ' + str(version),
            '\251 Copyright 2015 by Charles Sharman and Others')

    def save(self, name=''):
        """
        Saves a screen shot
        """
        global app

        if name:
            filename = name
        else:
            filename = str(
                _QtGui.QFileDialog.getSaveFileName(
                    self,
                    'Save Screen Image',
                    '.',
                    'Image Files (*.png *.bmp *.jpg *.gif)'))

        if filename:
            while app.hasPendingEvents():
                app.processEvents()
            retval = self.glarea.occ_view.Dump(filename)
            if not retval:
                self.status_bar.setText('Error: Couldn\'t save ' + filename)
            else:
                self.status_bar.setText('Saved ' + filename)

            # This works and allows higher resolutions
            #pixmap = Image_AlienPixMap()
            #size = self.glarea.size()
            #self.glarea.occ_view.ToPixMap(pixmap, size.width(), size.height())
            #pixmap.Save(TCollection_AsciiString(name))

    def perspective_length(self, distance):
        """
        Sets the focal length for perspective views
        """
        self.glarea.occ_view.SetFocale(distance)

    def perspective_angle(self, angle):
        """
        Sets the focal length for perspective views
        """
        self.glarea.occ_view.SetAngle(angle)

    def quit(self):
        """
        Closes the viewer
        """
        global __name__, app, interactive
        if __name__ == '__main__' or not interactive:
            app.quit()
        else:
            self.close()


class GLWidget(_QtGui.QWidget):
    """
    A ccad canvas
    """

    def __init__(self, parent=None):
        super(GLWidget, self).__init__(parent)
        self.setMouseTracking(True)
        #self.setFocusPolicy(_QtCore.Qt.WheelFocus)
        self.setAttribute(_QtCore.Qt.WA_PaintOnScreen)
        self.setAttribute(_QtCore.Qt.WA_NoSystemBackground)
        self.setSizePolicy(_QtGui.QSizePolicy(_QtGui.QSizePolicy.Expanding,
                                              _QtGui.QSizePolicy.Expanding))
        self.occ_view = None
        self.SCR = (400, 400)

    def start(self, perspective=False):

        # Set up the OCC hooks to the OpenGL space
        window_handle = int(self.winId())

        self.d3d = _Display3d()
        self.d3d.Init(window_handle)

        handle_occ_context = self.d3d.GetContext()
        handle_occ_viewer = self.d3d.GetViewer()
        self.handle_view = self.d3d.GetView()
        self.occ_context = handle_occ_context.GetObject()
        self.occ_viewer = handle_occ_viewer.GetObject()
        self.occ_viewer.SetDefaultLights()
        self.occ_viewer.SetLightOn()
        self.occ_view = self.handle_view.GetObject()

    def sizeHint(self):
        return _QtCore.QSize(self.SCR[0], self.SCR[1])

    def paintEvent(self, event):
        if self.occ_viewer:
            self.occ_viewer.Redraw()

    def resizeEvent(self, event):
        global app
        x, y = event.size().width(), event.size().height()
        self.SCR = (x, y)
        self.mpan = max(x, y) / 10

        if self.occ_view:
            # There's a race condition here.  The resize must be known
            # to qt before MustBeResized is called.
            while app.hasPendingEvents():
                app.processEvents()
            self.occ_view.MustBeResized()

    def paintEngine(self):
        return None


def view(perspective=False):
    global manager, app

    if manager == 'qt':
        if not app:
            app = _QtGui.QApplication([])
        v1 = view_qt(perspective)
        return v1
    else:
        print('Error: Manager', manager, 'not supported')


def start():  # For non-interactive sessions (don't run in ipython)
    global interactive, manager, app
    interactive = False

    if manager == 'qt':
        app.exec_()
    else:
        print('Error: Manager', manager, 'not supported')


if __name__ == '__main__':
    import model as cm
    view = view_qt()
    view.set_background((0.35, 0.35, 0.35))
    s1 = cm.sphere(1.0)
    view.display(s1, (0.5, 0.0, 0.0), line_type='solid', line_width=3)
    s2 = cm.box(1, 2, 3)
    view.display(s2,
                 (0.0, 0.0, 0.5),
                 transparency=0.5,
                 line_type='dash',
                 line_width=1)
    start()