#-------------------------------------------------------------------------------
#
#  Copyright (c) 2007, Enthought, Inc.
#  All rights reserved.
#
#  This software is provided without warranty under the terms of the BSD
#  license included in enthought/LICENSE.txt and may be redistributed only
#  under the conditions described in the aforementioned license.  The license
#  is also available online at http://www.enthought.com/licenses/BSD.txt
#
#  Thanks for using Enthought open source!
#
#  Author: David C. Morrill
#  Date:   03/30/2007
#
#  fixme:
#  - Get custom tree view images.
#  - Write a program to create a directory structure from a lesson plan file.
#
#-------------------------------------------------------------------------------

""" A framework for creating interactive Python tutorials.
"""

#-------------------------------------------------------------------------------
#  Imports:
#-------------------------------------------------------------------------------

import sys
import os
import re

from string \
    import capwords

from traits.api \
    import HasPrivateTraits, HasTraits, File, Directory, Instance, Int, Str, \
           List, Bool, Dict, Any, Property, Delegate, Button, cached_property

from traitsui.api \
    import View, VGroup, HGroup, VSplit, HSplit, Tabbed, Item, Heading, \
           Handler, ListEditor, CodeEditor, EnumEditor, HTMLEditor, \
           TreeEditor, TitleEditor, ValueEditor, ShellEditor

from traitsui.menu \
    import NoButtons

from traitsui.tree_node \
    import TreeNode

from pyface.image_resource \
    import ImageResource

try:
    from traitsui.wx.extra.windows.ie_html_editor \
        import IEHTMLEditor

    from traitsui.wx.extra.windows.flash_editor \
        import FlashEditor
except:
    IEHTMLEditor = FlashEditor = None

#-------------------------------------------------------------------------------
#  Constants:
#-------------------------------------------------------------------------------

# Correct program usage information:
Usage = """
Correct usage is: tutor.py [root_dir]
where:
    root_dir = Path to root of the tutorial tree

If omitted, 'root_dir' defaults to the current directory."""

# The standard list editor used:
list_editor = ListEditor(
    use_notebook = True,
    deletable    = False,
    page_name    = '.title',
    export       = 'DockWindowShell',
    dock_style   = 'fixed'
)

# The standard code snippet editor used:
snippet_editor = ListEditor(
    use_notebook = True,
    deletable    = False,
    page_name    = '.title',
    export       = 'DockWindowShell',
    dock_style   = 'tab',
    selected     = 'snippet'
)

# Regular expressions used to match section directories:
dir_pat1 = re.compile( r'^(\d\d\d\d)_(.*)$' )
dir_pat2 = re.compile( r'^(.*)_(\d+\.\d+)$' )

# Regular expression used to match section header in a Python source file:
section_pat1 = re.compile( r'^#-*\[(.*)\]' )  # Normal
section_pat2 = re.compile( r'^#-*<(.*)>' )    # Hidden
section_pat3 = re.compile( r'^#-*\((.*)\)' )  # Description

# Regular expression used to extract item titles from URLs:
url_pat1 = re.compile( r'^(.*)\[(.*)\](.*)$' )  # Normal

# Is this running on the Windows platform?
is_windows = (sys.platform in ( 'win32', 'win64' ))

# Python file section types:
IsCode        = 0
IsHiddenCode  = 1
IsDescription = 2

# HTML template for a default lecture:
DefaultLecture = """<html>
  <head>
  </head>
  <body>
    <p>This section contains the following topics:</p>
    <ul>
    %s
    </ul>
  </body>
</html>
"""

# HTML template for displaying a .wmv/.avi movie file:
WMVMovieTemplate = """<html>
<head>
</head>
<body>
<p><object classid="clsid:22D6F312-B0F6-11D0-94AB-0080C74C7E95" codebase="http://activex.microsoft.com/activex/controls/mplayer/en/nsmp2inf.cab#Version=6,4,5,715">
<param name="FileName" value="%s">
<param name="AutoStart" value="true">
<param name="ShowTracker" value="true">
<param name="ShowControls" value="true">
<param name="ShowGotoBar" value="false">
<param name="ShowDisplay" value="false">
<param name="ShowStatusBar" value="false">
<param name="AutoSize" value="true">
<embed src="%s" AutoStart="true" ShowTracker="true" ShowControls="true" ShowGotoBar="false" ShowDisplay="false" ShowStatusBar="false" AutoSize="true" pluginspage="http://www.microsoft.com/windows/windowsmedia/download/"></object></p>
</body>
</html>
"""

# HTML template for displaying a QuickTime.mov movie file:
QTMovieTemplate = """<html>
<head>
</head>
<body>
<p><object classid="clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B" codebase="http://www.apple.com/qtactivex/qtplugin.cab" width="100%%" height="100%%">
<param name="src" value="file:///%s">
<param name="scale" value="aspect">
<param name="autoplay" value="true">
<param name="loop" value="false">
<param name="controller" value="true">
<embed src="file:///%s" width="100%%" height="100%%" scale="aspect" autoplay="true" loop="false" controller="true" pluginspage="http://www.apple.com/quicktime/download"></object></p>
</body>
</html>
"""

# HTML template for displaying an image file:
ImageTemplate = """<html>
<head>
</head>
<body>
<img src="%s">
</body>
</html>
"""

# HTML template for playing an MP3 audio file:
MP3Template = """<html>
<head>
<bgsound src="%s">
</head>
<body>
<p>&nbsp;</p>
</body>
</html>
"""

#-------------------------------------------------------------------------------
#  Returns the contents of a specified text file (or None):
#-------------------------------------------------------------------------------

def read_file ( path, mode = 'rb' ):
    """ Returns the contents of a specified text file (or None).
        """
    fh = result = None

    try:
        fh     = file( path, mode )
        result = fh.read()
    except:
        pass

    if fh is not None:
        try:
            fh.close()
        except:
            pass

    return result

#-------------------------------------------------------------------------------
#  Creates a title from a specified string:
#-------------------------------------------------------------------------------

def title_for ( title ):
    """ Creates a title from a specified string.
    """
    return capwords( title.replace( '_', ' ' ) )

#-------------------------------------------------------------------------------
#  Returns a relative CSS style sheet path for a specified path and parent
#  section:
#-------------------------------------------------------------------------------

def css_path_for ( path, parent ):
    """ Returns a relative CSS style sheet path for a specified path and parent
        section.
    """
    if os.path.isfile( os.path.join( path, 'default.css' ) ):
        return 'default.css'

    if parent is not None:
        result = parent.css_path
        if result != '':
            if path != parent.path:
                result = os.path.join( '..', result )

            return result

    return ''

#-------------------------------------------------------------------------------
#  'StdOut' class:
#-------------------------------------------------------------------------------

class StdOut ( object ):
    """ Simulate stdout, but redirect the output to the 'output' string
        supplied by some 'owner' object.
    """

    def __init__ ( self, owner ):
        self.owner = owner

    def write ( self, data ):
        """ Adds the specified data to the output log.
        """
        self.owner.output += data

    def flush ( self ):
        """ Flushes all current data to the output log.
        """
        pass

#-------------------------------------------------------------------------------
#  'NoDemo' class:
#-------------------------------------------------------------------------------

class NoDemo ( HasPrivateTraits ):

    #-- Traits View Definitions ------------------------------------------------

    view = View(
        Heading( 'No demo defined for this lab.' ),
        resizable = True
    )

#-------------------------------------------------------------------------------
#  'DemoPane' class:
#-------------------------------------------------------------------------------

class DemoPane ( HasPrivateTraits ):
    """ Displays the contents of a Python lab's *demo* value.
    """

    #-- Trait Definitions ------------------------------------------------------

    demo = Instance( HasTraits, factory = NoDemo )

    #-- Traits View Definitions ------------------------------------------------

    view = View(
        Item( 'demo',
              id         = 'demo',
              show_label = False,
              style      = 'custom',
              resizable  = True
        ),
        id        = 'enthought.tutor.demo',
        resizable = True
    )

#-------------------------------------------------------------------------------
#  'ATutorialItem' class:
#-------------------------------------------------------------------------------

class ATutorialItem ( HasPrivateTraits ):
    """ Defines the abstract base class for each type of item (HTML, Flash,
        text, code) displayed within the tutor.
    """

    #-- Traits Definitions -----------------------------------------------------

    # The title for the item:
    title = Str

    # The path to the item:
    path = File

    # The displayable content for the item:
    content = Property

#-------------------------------------------------------------------------------
#  'ADescriptionItem' class:
#-------------------------------------------------------------------------------

class ADescriptionItem ( ATutorialItem ):
    """ Defines a common base class for all description items.
    """

    #-- Event Handlers ---------------------------------------------------------

    def _path_changed ( self, path ):
        """ Sets the title for the item based on the item's path name.
        """
        self.title = title_for( os.path.splitext( os.path.basename(
                                                  path ) )[0] )

#-------------------------------------------------------------------------------
#  'HTMLItem' class:
#-------------------------------------------------------------------------------

class HTMLItem ( ADescriptionItem ):
    """ Defines a class used for displaying a single HTML page within the tutor
        using the default Traits HTML editor.
    """

    #-- Traits Definitions -----------------------------------------------------

    url = Str

    #-- Traits View Definitions ------------------------------------------------

    view = View(
        Item( 'content',
              style      = 'readonly',
              show_label = False,
              editor     = HTMLEditor()
        )
    )

    #-- Event Handlers ---------------------------------------------------------

    def _url_changed ( self, url ):
        """ Sets the item title when the 'url' is changed.
        """
        match = url_pat1.match( url )
        if match is not None:
            title = match.group(2).strip()
        else:
            title = url.strip()
            col   = title.rfind( '/' )
            if col >= 0:
                title = os.path.splitext( title[ col + 1: ] )[0]

        self.title = title

    #-- Property Implementations -----------------------------------------------

    @cached_property
    def _get_content ( self ):
        """ Returns the item content.
        """
        url = self.url
        if url != '':
            match = url_pat1.match( url )
            if match is not None:
                url = match.group(1) + match.group(3)

            return url

        return read_file( self.path )

    def _set_content ( self, content ):
        """ Sets the item content.
        """
        self._content = content

#-------------------------------------------------------------------------------
#  'HTMLStrItem' class:
#-------------------------------------------------------------------------------

class HTMLStrItem ( HTMLItem ):
    """ Defines a class used for displaying a single HTML text string within
        the tutor using the default Traits HTML editor.
    """

    # Make the content a real trait rather than a property:
    content = Str

#-------------------------------------------------------------------------------
#  'IEHTMLItem' class:
#-------------------------------------------------------------------------------

class IEHTMLItem ( HTMLItem ):
    """ Defines a class used for displaying a single HTML page within the tutor
        using the Traits Internet Explorer HTML editor.
    """

    #-- Traits View Definitions ------------------------------------------------

    view = View(
        Item( 'content',
              style      = 'readonly',
              show_label = False,
              editor     = IEHTMLEditor()
        )
    )

#-------------------------------------------------------------------------------
#  'IEHTMLStrItem' class:
#-------------------------------------------------------------------------------

class IEHTMLStrItem ( IEHTMLItem ):
    """ Defines a class used for displaying a single HTML text string within
        the tutor using the Traits Internet Explorer HTML editor.
    """

    # Make the content a real trait rather than a property:
    content = Str

#-------------------------------------------------------------------------------
#  'FlashItem' class:
#-------------------------------------------------------------------------------

class FlashItem ( HTMLItem ):
    """ Defines a class used for displaying a Flash-based animation or video
        within the tutor.
    """

    #-- Traits View Definitions ------------------------------------------------

    view = View(
        Item( 'content',
              style      = 'readonly',
              show_label = False,
              editor     = FlashEditor()
        )
    )

#-------------------------------------------------------------------------------
#  'TextItem' class:
#-------------------------------------------------------------------------------

class TextItem ( ADescriptionItem ):
    """ Defines a class used for displaying a text file within the tutor.
    """

    #-- Traits View Definitions ------------------------------------------------

    view = View(
        Item( 'content',
              style      = 'readonly',
              show_label = False,
              editor     = CodeEditor( show_line_numbers = False,
                                       selected_color    = 0xFFFFFF )
        )
    )

    #-- Property Implementations -----------------------------------------------

    @cached_property
    def _get_content ( self ):
        """ Returns the item content.
        """
        return read_file( self.path )

#-------------------------------------------------------------------------------
#  'TextStrItem' class:
#-------------------------------------------------------------------------------

class TextStrItem ( TextItem ):
    """ Defines a class used for displaying a text file within the tutor.
    """

    # Make the content a real trait, rather than a property:
    content = Str

#-------------------------------------------------------------------------------
#  'CodeItem' class:
#-------------------------------------------------------------------------------

class CodeItem ( ATutorialItem ):
    """ Defines a class used for displaying a Python source code fragment
        within the tutor.
    """

    #-- Trait Definitions ------------------------------------------------------

    # The displayable content for the item (override):
    content = Str

    # The starting line of the code snippet within the original file:
    start_line = Int

    # The currently selected line:
    selected_line = Int

    # Should this section normally be hidden?
    hidden = Bool

    #-- Traits View Definitions ------------------------------------------------

    view = View(
        Item( 'content',
              style      = 'custom',
              show_label = False,
              editor     = CodeEditor( selected_line = 'selected_line' )
        )
    )

#-------------------------------------------------------------------------------
#  'ASection' abstract base class:
#-------------------------------------------------------------------------------

class ASection ( HasPrivateTraits ):
    """ Defines an abstract base class for a single section of a tutorial.
    """

    #-- Traits Definitions -----------------------------------------------------

    # The title of the section:
    title = Str

    # The path to this section:
    path = Directory

    # The parent section of this section (if any):
    parent = Instance( 'ASection' )

    # Optional table of contents (can be used to define/locate the subsections):
    toc = List( Str )

    # The path to the CSS style sheet to use for this section:
    css_path = Property

    # The list of subsections contained in this section:
    subsections = Property # List( ASection )

    # This section can be executed:
    is_runnable = Bool( True )

    # Should the Python code be automatically executed on start-up?
    auto_run = Bool( False )

    #-- Property Implementations -----------------------------------------------

    @cached_property
    def _get_subsections ( self ):
        """ Returns the subsections for this section:
        """
        if len( self.toc ) > 0:
            self._load_toc()
        else:
            self._load_dirs()

        # Return the cached list of sections:
        return self._subsections

    @cached_property
    def _get_css_path ( self ):
        """ Returns the path to the CSS style sheet for this section.
        """
        return css_path_for( self.path, self.parent )

    #-- Private Methods --------------------------------------------------------

    def _load_dirs ( self ):
        """ Defines the section's subsections by analyzing all of the section's
            sub-directories.
        """
        # No value cached yet:
        dirs = []
        path = self.path

        # Find every sub-directory whose name begins with a number of the
        # form ddd, or ends with a number of the form _ddd.ddd (used for
        # sorting them into the correct presentation order):
        for name in os.listdir( path ):
            dir = os.path.join( path, name )
            if os.path.isdir( dir ):
               match = dir_pat1.match( name )
               if match is not None:
                   dirs.append( ( float( match.group(1) ),
                                  match.group(2), dir ) )
               else:
                   match = dir_pat2.match( name )
                   if match is not None:
                       dirs.append( ( float( match.group(2) ),
                                      match.group(1), dir ) )

        # Sort the directories by their index value:
        dirs.sort( lambda l, r: cmp( l[0], r[0] ) )

        # Create the appropriate type of section for each valid directory:
        self._subsections = [
            sf.section for sf in [
                SectionFactory( title  = title_for( title ),
                                parent = self ).set(
                                path   = dir )
                for index, title, dir in dirs
            ] if sf.section is not None
        ]

    def _load_toc ( self ):
        """ Defines the section's subsections by finding matches for the items
            defined in the section's table of contents.
        """
        toc         = self.toc
        base_names  = [ item.split( ':', 1 )[0] for item in toc ]
        subsections = [ None ] * len( base_names )
        path        = self.path

        # Classify all file names that match a base name in the table of
        # contents:
        for name in os.listdir( path ):
            try:
                base_name = os.path.splitext( os.path.basename( name ) )[0]
                index     = base_names.index( base_name )
                if subsections[ index ] is None:
                    subsections[ index ] = []
                subsections[ index ].append( name )
            except:
                pass

        # Try to convert each group of names into a section:
        for i, names in enumerate( subsections ):

            # Only process items for which we found at least one matching file
            # name:
            if names is not None:

                # Get the title for the section from its table of contents
                # entry:
                parts = toc[i].split( ':', 1 )
                if len( parts ) == 1:
                    title = title_for( parts[0].strip() )
                else:
                    title = parts[1].strip()

                # Handle an item with one file which is a directory as a normal
                # section:
                if len( names ) == 1:
                    dir = os.path.join( path, names[0] )
                    if os.path.isdir( dir ):
                        subsections[i] = SectionFactory( title  = title,
                                                         parent = self ).set(
                                                         path   = dir ).section
                        continue

                # Otherwise, create a section from the list of matching files:
                subsections[i] = SectionFactory( title  = title,
                                                 parent = self,
                                                 files  = names ).set(
                                                 path   = path ).section

        # Set the subsections to the non-None values that are left:
        self._subsections = [ subsection for subsection in subsections
                                         if subsection is not None ]

#-------------------------------------------------------------------------------
#  'Lecture' class:
#-------------------------------------------------------------------------------

class Lecture ( ASection ):
    """ Defines a lecture, which is a section of a tutorial with descriptive
        information, but no associated Python code. Can be used to provide
        course overviews, introductory sections, or lead-ins to follow-on
        lessons or labs.
    """

    #-- Trait Definitions-------------------------------------------------------

    # The list of descriptive items for the lecture:
    descriptions = List( ATutorialItem )

    # This section can be executed (override):
    is_runnable = False

    #-- Traits View Definitions ------------------------------------------------

    view = View(
        Item( 'descriptions',
              style      = 'custom',
              show_label = False,
              editor     = list_editor
        ),
        id = 'enthought.tutor.lecture'
    )

#-------------------------------------------------------------------------------
#  'LabHandler' class:
#-------------------------------------------------------------------------------

class LabHandler ( Handler ):
    """ Defines the controller functions for the Lab view.
    """

    def init ( self, info ):
        """ Handles initialization of the view.
        """
        # Run the associated Python code if the 'auto-run' feature is enabled:
        if info.object.auto_run:
            info.object.run_code()

#-------------------------------------------------------------------------------
#  'Lab' class:
#-------------------------------------------------------------------------------

class Lab ( ASection ):
    """ Defines a lab, which is a section of a tutorial with only Python code.
        This type of section might typically follow a lecture which introduced
        the code being worked on in the lab.
    """

    #-- Trait Definitions-------------------------------------------------------

    # The set-up code (if any) for the lab:
    setup = Instance( CodeItem )

    # The list of code items for the lab:
    snippets = List( CodeItem )

    # The list of visible code items for the lab:
    visible_snippets = Property( depends_on = 'visible', cached = True )

    # The currently selected snippet:
    snippet = Instance( CodeItem )

    # Should normally hidden code items be shown?
    visible = Bool( False )

    # The dictionary containing the items from the Python code execution:
    values = Dict #Any( {} )

    # The run Python code button:
    run = Button( image = ImageResource( 'run' ), height_padding = 1 )

    # User error message:
    message = Str

    # The output produced while the program is running:
    output = Str

    # The current demo pane (if any):
    demo = Instance( DemoPane, () )

    #-- Traits View Definitions ------------------------------------------------

    view = View(
        VSplit(
            VGroup(
                Item( 'visible_snippets',
                      style      = 'custom',
                      show_label = False,
                      editor     = snippet_editor
                ),
                HGroup(
                    Item( 'run',
                          style      = 'custom',
                          show_label = False,
                          tooltip    = 'Run the Python code'
                    ),
                    '_',
                    Item( 'message',
                          springy    = True,
                          show_label = False,
                          editor     = TitleEditor()
                    ),
                    '_',
                    Item( 'visible',
                          label = 'View hidden sections'
                    )
                ),
            ),
            Tabbed(
                Item( 'values',
                      id     = 'values_1',
                      label  = 'Shell',
                      editor = ShellEditor( share = True ),
                      dock   = 'tab',
                      export = 'DockWindowShell'
                ),
                Item( 'values',
                      id     = 'values_2',
                      editor = ValueEditor(),
                      dock   = 'tab',
                      export = 'DockWindowShell'
                ),
                Item( 'output',
                      style  = 'readonly',
                      editor = CodeEditor( show_line_numbers = False,
                                           selected_color    = 0xFFFFFF ),
                      dock   = 'tab',
                      export = 'DockWindowShell'
                ),
                Item( 'demo',
                      id        = 'demo',
                      style     = 'custom',
                      resizable = True,
                      dock      = 'tab',
                      export    = 'DockWindowShell'
                ),
                show_labels = False,
            ),
            id = 'splitter',
        ),
        id      = 'enthought.tutor.lab',
        handler = LabHandler
    )

    #-- Event Handlers ---------------------------------------------------------

    def _run_changed ( self ):
        """ Runs the current set of snippet code.
        """
        self.run_code()

    #-- Property Implementations -----------------------------------------------

    @cached_property
    def _get_visible_snippets ( self ):
        """ Returns the list of code items that are currently visible.
        """
        if self.visible:
            return self.snippets

        return [ snippet for snippet in self.snippets if (not snippet.hidden) ]

    #-- Public Methods ---------------------------------------------------------

    def run_code ( self ):
        """ Runs all of the code snippets associated with the section.
        """
        # Reconstruct the lab code from the current set of code snippets:
        start_line = 1
        module     = ''
        for snippet in self.snippets:
            snippet.start_line = start_line
            module      = '%s\n\n%s' % ( module, snippet.content )
            start_line += (snippet.content.count( '\n' ) + 2)

        # Reset any syntax error and message log values:
        self.message   = self.output = ''

        # Redirect standard out and error to the message log:
        stdout, stderr = sys.stdout, sys.stderr
        sys.stdout     = sys.stderr = StdOut( self )

        try:
            try:
                # Get the execution context dictionary:
                values = self.values

                # Clear out any special variables defined by the last run:
                for name in ( 'demo', 'popup' ):
                    if isinstance( values.get( name ), HasTraits ):
                        del values[ name ]

                # Execute the current lab code:
                exec module[2:] in values, values

                # fixme: Hack trying to update the Traits UI view of the dict.
                self.values = {}
                self.values = values

                # Handle a 'demo' value being defined:
                demo = values.get( 'demo' )
                if not isinstance( demo, HasTraits ):
                    demo = NoDemo()
                self.demo.demo = demo

                # Handle a 'popup' value being defined:
                popup = values.get( 'popup' )
                if isinstance( popup, HasTraits ):
                    popup.edit_traits( kind = 'livemodal' )

            except SyntaxError, excp:
                # Convert the line number of the syntax error from one in the
                # composite module to one in the appropriate code snippet:
                line = excp.lineno
                if line is not None:
                    snippet = self.snippets[0]
                    for s in self.snippets:
                        if s.start_line > line:
                            break
                        snippet = s
                    line -= (snippet.start_line - 1)

                    # Highlight the line in error:
                    snippet.selected_line = line

                    # Select the correct code snippet:
                    self.snippet = snippet

                    # Display the syntax error message:
                    self.message = '%s in column %s of line %s' % (
                                   excp.msg.capitalize(), excp.offset, line )
                else:
                    # Display the syntax error message without line # info:
                    self.message = excp.msg.capitalize()
            except:
                import traceback
                traceback.print_exc()
        finally:
            # Restore standard out and error to their original values:
            sys.stdout, sys.stderr = stdout, stderr

#-------------------------------------------------------------------------------
#  'Lesson' class:
#-------------------------------------------------------------------------------

class Lesson ( Lab ):
    """ Defines a lesson, which is a section of a tutorial with both descriptive
        information and associated Python code.
    """

    #-- Trait Definitions-------------------------------------------------------

    # The list of descriptive items for the lesson:
    descriptions = List( ATutorialItem )

    #-- Traits View Definitions ------------------------------------------------

    view = View(
        HSplit(
            Item( 'descriptions',
                  label      = 'Lesson',
                  style      = 'custom',
                  show_label = False,
                  dock       = 'horizontal',
                  editor     = list_editor
            ),
            VSplit(
                VGroup(
                    Item( 'visible_snippets',
                          style      = 'custom',
                          show_label = False,
                          editor     = snippet_editor
                    ),
                    HGroup(
                        Item( 'run',
                              style      = 'custom',
                              show_label = False,
                              tooltip    = 'Run the Python code'
                        ),
                        '_',
                        Item( 'message',
                              springy    = True,
                              show_label = False,
                              editor     = TitleEditor()
                        ),
                        '_',
                        Item( 'visible',
                              label = 'View hidden sections'
                        )
                    ),
                    label = 'Lab',
                    dock  = 'horizontal'
                ),
                Tabbed(
                    Item( 'values',
                          id     = 'values_1',
                          label  = 'Shell',
                          editor = ShellEditor( share = True ),
                          dock   = 'tab',
                          export = 'DockWindowShell'

                    ),
                    Item( 'values',
                          id     = 'values_2',
                          editor = ValueEditor(),
                          dock   = 'tab',
                          export = 'DockWindowShell'
                    ),
                    Item( 'output',
                          style  = 'readonly',
                          editor = CodeEditor( show_line_numbers = False,
                                               selected_color    = 0xFFFFFF ),
                          dock   = 'tab',
                          export = 'DockWindowShell'
                    ),
                    Item( 'demo',
                          id        = 'demo',
                          style     = 'custom',
                          resizable = True,
                          dock      = 'tab',
                          export    = 'DockWindowShell'
                    ),
                    show_labels = False,
                ),
                label = 'Lab',
                dock  = 'horizontal'
            ),
            id = 'splitter',
        ),
        id      = 'enthought.tutor.lesson',
        handler = LabHandler
    )

#-------------------------------------------------------------------------------
#  'Demo' class:
#-------------------------------------------------------------------------------

class Demo ( Lesson ):
    """ Defines a demo, which is a section of a tutorial with both descriptive
        information and associated Python code which is executed but not
        shown.
    """

    #-- Traits View Definitions ------------------------------------------------

    view = View(
        HSplit(
            Item( 'descriptions',
                  label      = 'Lesson',
                  style      = 'custom',
                  show_label = False,
                  dock       = 'horizontal',
                  editor     = list_editor
            ),
            Item( 'demo',
                  id         = 'demo',
                  style      = 'custom',
                  show_label = False,
                  resizable  = True,
                  dock       = 'horizontal',
                  export     = 'DockWindowShell'
            ),
            id = 'splitter',
        ),
        id      = 'enthought.tutor.demo',
        handler = LabHandler
    )

#-------------------------------------------------------------------------------
#  'SectionFactory' class:
#-------------------------------------------------------------------------------

class SectionFactory ( HasPrivateTraits ):
    """ Defines a class that creates Lecture, Lesson or Lab sections (or None),
        based on the content of a specified directory. None is returned if the
        directory does not contain any recognized files.
    """

    #-- Traits Definitions -----------------------------------------------------

    # The path the section is to be created for:
    path = Directory

    # The list of files contained in the section:
    files = List( Str )

    # The parent of the section being created:
    parent = Instance( ASection )

    # The section created from the path:
    section = Instance( ASection )

    # The title for the section:
    title = Str

    # The optional table of contents for the section:
    toc = List( Str )

    # The list of descriptive items for the section:
    descriptions = List( ADescriptionItem )

    # The list of code snippet items for the section:
    snippets = List( CodeItem )

    # The path to the CSS style sheet for the section:
    css_path = Property

    # Should the Python code be automatically executed on start-up?
    auto_run = Bool( False )

    #-- Event Handlers ---------------------------------------------------------

    def _path_changed ( self, path ):
        """ Creates the appropriate section based on the value of the path.
        """
        # Get the list of files to process:
        files = self.files
        if len( files ) == 0:
            # If none were specified, then use all files in the directory:
            files = os.listdir( path )

            # Process the description file (if any) first:
            for name in files:
                if os.path.splitext( name )[1] == '.desc':
                    self._add_desc_item( os.path.join( path, name ) )
                    break

        # Try to convert each file into one or more 'xxxItem' objects:
        toc = [ item.split( ':', 1 )[0].strip() for item in self.toc ]
        for name in files:
            file_name = os.path.join( path, name )

            # Only process the ones that are actual files:
            if os.path.isfile( file_name ):

                # Use the file extension to determine the file's type:
                root, ext = os.path.splitext( name )
                if (root not in toc) and (len( ext ) > 1):

                    # If we have a handler for the file type, invoke it:
                    method = getattr( self, '_add_%s_item' % ext[1:].lower(),
                                      None )
                    if method is not None:
                        method( file_name )

        # Based on the type of items created (if any), create the corresponding
        # type of section:
        if len( self.descriptions ) > 0:
            if len( self.snippets ) > 0:
                if len( [ snippet for snippet in self.snippets
                                  if (not snippet.hidden) ] ) > 0:
                    self.section = Lesson(
                        title        = self.title,
                        path         = path,
                        toc          = self.toc,
                        parent       = self.parent,
                        descriptions = self.descriptions,
                        snippets     = self.snippets,
                        auto_run     = self.auto_run
                    )
                else:
                    self.section = Demo(
                        title        = self.title,
                        path         = path,
                        toc          = self.toc,
                        parent       = self.parent,
                        descriptions = self.descriptions,
                        snippets     = self.snippets,
                        auto_run     = True
                    )
            else:
                self.section = Lecture(
                    title        = self.title,
                    path         = path,
                    toc          = self.toc,
                    parent       = self.parent,
                    descriptions = self.descriptions
                )
        elif len( self.snippets ) > 0:
            self.section = Lab(
                title    = self.title,
                path     = path,
                toc      = self.toc,
                parent   = self.parent,
                snippets = self.snippets,
                auto_run = self.auto_run
            )
        else:
            # No descriptions or code snippets were found. Create a lecture
            # anyway:
            section = Lecture(
                title  = self.title,
                path   = path,
                toc    = self.toc,
                parent = self.parent
            )

            # If the lecture has subsections, then return the lecture and add
            # a default item containing a description of the subsections of the
            # lecture:
            if len( section.subsections ) > 0:
                self._create_html_item( path = path, content =
                         DefaultLecture % ( '\n'.join(
                             [ '<li>%s</li>' % subsection.title
                               for subsection in section.subsections ] ) ) )
                section.descriptions = self.descriptions
                self.section = section

    #-- Property Implementations -----------------------------------------------

    def _get_css_path ( self ):
        """ Returns the path to the CSS style sheet for the section.
        """
        return css_path_for( self.path, self.parent )

    #-- Factory Methods for Creating Section Items Based on File Type ----------

    def _add_py_item ( self, path ):
        """ Creates the code snippets for a Python source file.
        """
        source = read_file( path )
        if source is not None:
            lines      = source.replace( '\r', '' ).split( '\n' )
            start_line = 0
            title      = 'Prologue'
            type       = IsCode

            for i, line in enumerate( lines ):
                match = section_pat1.match( line )
                if match is not None:
                    next_type = IsCode
                else:
                    match = section_pat2.match( line )
                    if match is not None:
                        next_type = IsHiddenCode
                    else:
                        next_type = IsDescription
                        match     = section_pat3.match( line )

                if match is not None:
                    self._add_snippet( title, path, lines, start_line, i - 1,
                                       type )
                    start_line = i + 1
                    title      = match.group(1).strip()
                    type       = next_type

            self._add_snippet( title, path, lines, start_line, i, type )

    def _add_txt_item ( self, path ):
        """ Creates a description item for a normal text file.
        """
        self.descriptions.append( TextItem( path = path ) )

    def _add_htm_item ( self, path ):
        """ Creates a description item for an HTML file.
        """
        # Check if there is a corresponding .rst (restructured text) file:
        dir, base_name = os.path.split( path )
        rst = os.path.join( dir, os.path.splitext( base_name )[0] + '.rst' )

        # If no .rst file exists, just add the file as a normal HTML file:
        if not os.path.isfile( rst ):
            self._create_html_item( path = path )

    def _add_html_item ( self, path ):
        """ Creates a description item for an HTML file.
        """
        self._add_htm_item( path )

    def _add_url_item ( self, path ):
        """ Creates a description item for a file containing URLs.
        """
        data = read_file( path )
        if data is not None:
            for url in [ line for line in data.split( '\n' )
                              if line.strip()[:1] not in ( '', '#' ) ]:
                self._create_html_item( url = url.strip() )

    def _add_rst_item ( self, path ):
        """ Creates a description item for a ReSTructured text file.
        """
        # If docutils is not installed, just process the file as an ordinary
        # text file:
        try:
            from docutils.core import publish_cmdline
        except:
            self._add_txt_item( path )
            return

        # Get the name of the HTML file we will write to:
        dir, base_name = os.path.split( path )
        html = os.path.join( dir, os.path.splitext( base_name )[0] + '.htm' )

        # Try to find a CSS style sheet, and set up the docutil overrides if
        # found:
        settings = {}
        css_path = self.css_path
        if css_path != '':
            css_path = os.path.join( self.path, css_path )
            settings[ 'stylesheet_path' ]  = css_path
            settings[ 'embed_stylesheet' ] = True
            settings[ 'stylesheet' ]       = None
        else:
            css_path = path

        # If the HTML file does not exist, or is older than the restructured
        # text file, then let docutils convert it to HTML:
        is_file = os.path.isfile( html )
        if ((not is_file) or
            (os.path.getmtime( path )     > os.path.getmtime( html )) or
            (os.path.getmtime( css_path ) > os.path.getmtime( html ))):

            # Delete the current HTML file (if any):
            if is_file:
                os.remove( html )

            # Let docutils create a new HTML file from the restructured text
            # file:
            publish_cmdline( writer_name        = 'html',
                             argv               = [ path, html ],
                             settings_overrides = settings )

        if os.path.isfile( html ):
            # If there is now a valid HTML file, use it:
            self._create_html_item( path = html )

        else:
            # Otherwise, just use the original restructured text file:
            self._add_txt_item( path )

    def _add_swf_item ( self, path ):
        """ Creates a description item for a Flash file.
        """
        if is_windows:
            self.descriptions.append( FlashItem( path = path ) )

    def _add_mov_item ( self, path ):
        """ Creates a description item for a QuickTime movie file.
        """
        path2 = path.replace( ':', '|' )
        self._create_html_item( path    = path,
                                content = QTMovieTemplate % ( path2, path2 ) )

    def _add_wmv_item ( self, path ):
        """ Creates a description item for a Windows movie file.
        """
        self._create_html_item( path    = path,
                                content = WMVMovieTemplate % ( path, path ) )

    def _add_avi_item ( self, path ):
        """ Creates a description item for an AVI movie file.
        """
        self._add_wmv_item( path )

    def _add_jpg_item ( self, path ):
        """ Creates a description item for a JPEG image file.
        """
        self._create_html_item( path    = path,
                                content = ImageTemplate % path )

    def _add_jpeg_item ( self, path ):
        """ Creates a description item for a JPEG image file.
        """
        self._add_jpg_item( path )

    def _add_png_item ( self, path ):
        """ Creates a description item for a PNG image file.
        """
        self._add_jpg_item( path )

    def _add_mp3_item ( self, path ):
        """ Creates a description item for an mp3 audio file.
        """
        self._create_html_item( path    = path,
                                content = MP3Template % path )

    def _add_desc_item ( self, path ):
        """ Creates a section title from a description file.
        """
        # If we've already processed a description file, then we're done:
        if len( self.toc ) > 0:
            return

        lines = []
        desc  = read_file( path )
        if desc is not None:
            # Split the file into lines and save the non-empty, non-comment
            # lines:
            for line in desc.split( '\n' ):
                line = line.strip()
                if (len( line ) > 0) and (line[0] != '#'):
                    lines.append( line )

        if len( lines ) == 0:
            # If the file didn't have anything useful in it, set a title based
            # on the description file name:
            self.title = title_for(
                             os.path.splitext( os.path.basename( path ) )[0] )
        else:
            # Otherwise, set the title and table of contents from the lines in
            # the file:
            self.title = lines[0]
            self.toc   = lines[1:]

    #-- Private Methods --------------------------------------------------------

    def _add_snippet ( self, title, path, lines, start_line, end_line, type ):
        """ Adds a new code snippet or restructured text item to the list of
            code snippet or description items.
        """
        # Trim leading and trailing blank lines from the snippet:
        while start_line <= end_line:
            if lines[ start_line ].strip() != '':
                break
            start_line += 1

        while end_line >= start_line:
            if lines[ end_line ].strip() != '':
                break
            end_line -= 1

        # Only add if the snippet is not empty:
        if start_line <= end_line:

            # Check for the title containing the 'auto-run' flag ('*'):
            if title[:1] == '*':
                self.auto_run = True
                title = title[1:].strip()

            if title[-1:] == '*':
                self.auto_run = True
                title = title[:-1].strip()

            # Extract out just the lines we will use:
            content_lines = lines[ start_line: end_line + 1 ]

            if type == IsDescription:
                # Add the new restructured text description:
                self._add_description( content_lines, title )
            else:
                # Add the new code snippet:
                self.snippets.append( CodeItem(
                    title   = title or 'Code',
                    path    = path,
                    hidden  = (type == IsHiddenCode),
                    content = '\n'.join( content_lines )
                ) )

    def _add_description ( self, lines, title ):
        """ Converts a restructured text string to HTML and adds it as
            description item.
        """
        # Scan the lines for any imbedded Python code that should be shown as
        # a separate snippet:
        i = 0
        while i < len( lines ):
            if lines[i].strip()[-2:] == '::':
                i = self._check_embedded_code( lines, i + 1 )
            else:
                i += 1

        # Strip off any docstring style triple quotes (if necessary):
        content = '\n'.join( lines ).strip()
        if content[:3] in ( '"""', "'''" ):
            content = content[3:]

        if content[-3:] in ( '"""', "'''" ):
            content = content[:-3]

        content = content.strip()

        # If docutils is not installed, just add it as a text string item:
        try:
            from docutils.core import publish_string
        except:
            self.descriptions.append( TextStrItem( content = content,
                                                   title   = title ) )
            return

        # Try to find a CSS style sheet, and set up the docutil overrides if
        # found:
        settings = {}
        css_path = self.css_path
        if css_path != '':
            css_path = os.path.join( self.path, css_path )
            settings[ 'stylesheet_path' ]  = css_path
            settings[ 'embed_stylesheet' ] = True
            settings[ 'stylesheet' ]       = None

        # Convert it from restructured text to HTML:
        html = publish_string( content, writer_name        = 'html',
                                        settings_overrides = settings )

        # Choose the right HTML renderer:
        if is_windows:
            item = IEHTMLStrItem( content = html, title = title )
        else:
            item = HTMLStrItem( content = html, title = title )

        # Add the resulting item to the descriptions list:
        self.descriptions.append( item )

    def _create_html_item ( self, **traits ):
        """ Creates a platform specific html item and adds it to the list of
            descriptions.
        """
        if is_windows:
            item = IEHTMLItem( **traits )
        else:
            item = HTMLItem( **traits )

        self.descriptions.append( item )

    def _check_embedded_code ( self, lines, start ):
        """ Checks for an embedded Python code snippet within a description.
        """
        n = len( lines )
        while start < n:
            line = lines[ start ].strip()

            if line == '':
                start += 1
                continue

            if (line[:1] != '[') or (line[-1:] != ']'):
                break

            del lines[ start ]

            n     -= 1
            title  = line[1:-1].strip()
            line   = lines[ start ] + '.'
            pad    = len( line ) - len( line.strip() )
            clines = []

            while start < n:
                line     = lines[ start ] + '.'
                len_line = len( line.strip() )
                if (len_line > 1) and ((len( line ) - len_line) < pad):
                    break

                if (len( clines ) > 0) or (len_line > 1):
                    clines.append( line[ pad: -1 ] )

                start += 1

            # Add the new code snippet:
            self.snippets.append( CodeItem(
                title   = title or 'Code',
                content = '\n'.join( clines )
            ) )

            break

        return start

#-------------------------------------------------------------------------------
#  Tutor tree editor:
#-------------------------------------------------------------------------------

tree_editor = TreeEditor(
    nodes = [
        TreeNode(
            children   = 'subsections',
            label      = 'title',
            rename     = False,
            copy       = False,
            delete     = False,
            delete_me  = False,
            insert     = False,
            auto_open  = True,
            auto_close = False,
            node_for   = [ ASection ],
            icon_group = '<group>'
        )
    ],
    editable  = False,
    auto_open = 1,
    selected = 'section'
)

#-------------------------------------------------------------------------------
#  'Tutor' class:
#-------------------------------------------------------------------------------

class Tutor ( HasPrivateTraits ):
    """ The main tutorial class which manages the presentation and navigation
        of the entire tutorial.
    """

    #-- Trait Definitions ------------------------------------------------------

    # The path to the files distributed with the tutor:
    home = Directory

    # The path to the root of the tutorial tree:
    path = Directory

    # The root of the tutorial lesson tree:
    root = Instance( ASection )

    # The current section of the tutorial being displayed:
    section = Instance( ASection )

    # The next section:
    next_section = Property( depends_on = 'section', cached = True )

    # The previous section:
    previous_section = Property( depends_on = 'section', cached = True )

    # The previous section button:
    previous = Button( image = ImageResource( 'previous' ), height_padding = 1 )

    # The next section button:
    next = Button( image = ImageResource( 'next' ), height_padding = 1 )

    # The parent section button:
    parent = Button( image = ImageResource( 'parent' ), height_padding = 1 )

    # The reload tutor button:
    reload = Button( image = ImageResource( 'reload' ), height_padding = 1 )

    # The title of the current session:
    title = Property( depends_on = 'section' )

    #-- Traits View Definitions ------------------------------------------------

    view = View(
        VGroup(
            HGroup(
                Item( 'previous',
                      style        = 'custom',
                      enabled_when = 'previous_section is not None',
                      tooltip      = 'Go to previous section'
                ),
                Item( 'parent',
                      style        = 'custom',
                      enabled_when = '(section is not None) and '
                                     '(section.parent is not None)',
                      tooltip      = 'Go up one level'
                ),
                Item( 'next',
                      style        = 'custom',
                      enabled_when = 'next_section is not None',
                      tooltip      = 'Go to next section'
                ),
                '_',
                Item( 'title',
                      springy = True,
                      editor  = TitleEditor()
                ),
                '_',
                Item( 'reload',
                      style   = 'custom',
                      tooltip = 'Reload the tutorial'
                ),
                show_labels = False
            ),
            '_',
            HSplit(
                Item( 'root',
                      label  = 'Table of Contents',
                      editor = tree_editor,
                      dock   = 'horizontal',
                      export = 'DockWindowShell'
                ),
                Item( 'section',
                      id        = 'section',
                      label     = 'Current Lesson',
                      style     = 'custom',
                      resizable = True,
                      dock      = 'horizontal'
                ),
                id          = 'splitter',
                show_labels = False
            )
        ),
        title     = 'Python Tutor',
        id        = 'dmorrill.tutor.tutor:1.0',
        buttons   = NoButtons,
        resizable = True,
        width     = 0.8,
        height    = 0.8
    )

    #-- Event Handlers ---------------------------------------------------------

    def _path_changed ( self, path ):
        """ Handles the tutorial root path being changed.
        """
        self.init_tutor()

    def _next_changed ( self ):
        """ Displays the next tutorial section.
        """
        self.section = self.next_section

    def _previous_changed ( self ):
        """ Displays the previous tutorial section.
        """
        self.section = self.previous_section

    def _parent_changed ( self ):
        """ Displays the parent of the current tutorial section.
        """
        self.section = self.section.parent

    def _reload_changed ( self ):
        """ Reloads the tutor from the original path specified.
        """
        self.init_tutor()

    #-- Property Implementations -----------------------------------------------

    @cached_property
    def _get_next_section ( self ):
        """ Returns the next section of the tutorial.
        """
        next    = None
        section = self.section
        if len( section.subsections ) > 0:
            next = section.subsections[0]
        else:
            parent = section.parent
            while parent is not None:
                index = parent.subsections.index( section )
                if index < (len( parent.subsections ) - 1):
                    next = parent.subsections[ index + 1 ]
                    break

                parent, section = parent.parent, parent

        return next

    @cached_property
    def _get_previous_section ( self ):
        """ Returns the previous section of the tutorial.
        """
        previous = None
        section  = self.section
        parent   = section.parent
        if parent is not None:
            index = parent.subsections.index( section )
            if index > 0:
                previous = parent.subsections[ index - 1 ]
                while len( previous.subsections ) > 0:
                    previous = previous.subsections[-1]
            else:
                previous = parent

        return previous

    def _get_title ( self ):
        """ Returns the title of the current section.
        """
        section = self.section
        if section is None:
            return ''

        return ('%s: %s' % ( section.__class__.__name__, section.title ))

    #-- Public Methods ---------------------------------------------------------

    def init_tutor ( self ):
        """ Initials the tutor by creating the root section from the specified
            path.
        """
        path    = self.path
        title   = title_for( os.path.splitext( os.path.basename( path ) )[0] )
        section = SectionFactory( title = title ).set( path = path ).section
        if section is not None:
            self.section = self.root = section

#-------------------------------------------------------------------------------
#  Run the program:
#-------------------------------------------------------------------------------

# Only run the program if we were invoked from the command line:
if __name__ == '__main__':

    # Validate the command line arguments:
    if len( sys.argv ) > 2:
        print Usage
        sys.exit( 1 )

    # Determine the root path to use for the tutorial files:
    if len( sys.argv ) == 2:
        path = sys.argv[1]
    else:
        path = os.getcwd()

    # Create a tutor and display the tutorial:
    tutor = Tutor( home = os.path.dirname( sys.argv[0] ) ).set(
                   path = path )
    if tutor.root is not None:
        tutor.configure_traits()
    else:
        print """No traits tutorial found in %s.

Correct usage is: python tutor.py [tutorial_path]
where: tutorial_path = Path to the root of the traits tutorial.

If tutorial_path is omitted, the current directory is assumed to be the root of
the tutorial.""" % path

