// $Id: TransformableDynamicRenderer.cs 2012 2009-08-12 00:50:47Z jing $

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

using Microsoft.Ink;
using Microsoft.StylusInput;
using Microsoft.StylusInput.PluginData;

using UW.ClassroomPresenter.Model;
using UW.ClassroomPresenter.Model.Presentation;
using UW.ClassroomPresenter.Model.Viewer;
using UW.ClassroomPresenter.Viewer.Slides;

namespace UW.ClassroomPresenter.Viewer.Inking {
    /// <summary>
    /// A custom implementation of <see cref="DynamicRenderer"/> which allows
    /// for affine transforms of the displayed ink.
    /// </summary>
    /// <remarks>
    /// Since the "real" <see cref="DynamicRenderer"/> class is <c>sealed</c>,
    /// it was necessary to implement the renderer in a slightly less efficient
    /// manner.  The <see cref="TransformableDynamicRenderer"/> creates a small
    /// ink <see cref="Stroke"/> for each set of packet data received from the
    /// stylus, and renders it using a <see cref="Renderer"/> (having applied
    /// any geometric transformation to the ink).  The implementation is still
    /// <i>O(1)</i>, as only the relevant portion of the data is kept.
    /// </remarks>
    /// <remarks>
    /// <em>Known issue:</em> When using a <see cref="DrawingAttributes"/>
    /// with <see cref="DrawingAttributes.Transparency">Transparency</see> less
    /// than <c>255</c>, artifacts are introduced due to repeated drawing of ink
    /// over the same region.  Therefore, until Microsoft allows geometric transforms
    /// to be applied to its standard <see cref="DynamicRenderer"/>, the use of
    /// semi-transparent ink is not recommended.
    /// </remarks>
    /// <remarks>
    /// The <see cref="TransformableDynamicRenderer"/> renders ink using a
    /// <see cref="DrawingAttributes"/> object which is either specified via the
    /// <see cref="#DrawingAttributes"/> property, or which is received via
    /// a <see cref="CustomStylusDataAdded"/> event generated by a
    /// <see cref="StylusInputSelector"/> connected to the same
    /// <see cref="RealTimeStylus"/>.
    /// </remarks>
    /// <remarks>
    /// The <see cref="TransformableDynamicRenderer"/> class is essentially a wrapper
    /// around a <see cref="Core"/> instance.  The <see cref="TransformableDynamicRenderer"/>
    /// is to be used as a <see cref="RealTimeStylus"/> plugin, while the <see cref="Core"/>
    /// class should be used to render ink outside of the context of a <see cref="RealTimeStylus"/>.
    /// </remarks>
    public class TransformableDynamicRenderer : IStylusSyncPlugin, IDisposable {
        private readonly Core m_Core;
        private bool m_Disposed;

        #region Construction & Destruction

        /// <summary>
        /// Constructs a new <see cref="TransformableDynamicRenderer"/> instance, which will
        /// render ink to the graphics device returned by the <see cref="SlideDisplayModel"/>.
        /// </summary>
        /// <remarks>
        /// Furthermore, the ink will be transformed using the
        /// <see cref="SlideDisplayModel.InkTransform"/>
        /// and clipped by <see cref="SlideDisplayModel.Bounds"/>.
        /// </remarks>
        public TransformableDynamicRenderer(SlideDisplayModel display) {
            this.m_Core = new Core(display);
        }

        ~TransformableDynamicRenderer() {
            this.Dispose(false);
        }

        public void Dispose() {
            this.Dispose(true);
            System.GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing) {
            if(this.m_Disposed) return;
            if(disposing) {
                m_Core.Dispose();
            }
            this.m_Disposed = true;
        }

        #endregion Construction & Destruction


        #region Core Wrapper

        /// <summary>
        /// Gets or sets the <see cref="DrawingAttributes"/> that will be used when rendering ink
        /// packets received from the <see cref="RealTimeStylus"/>.
        /// </summary>
        public DrawingAttributes DrawingAttributes {
            get { return this.m_Core.DrawingAttributes; }
            set { this.m_Core.DrawingAttributes = value; }
        }

        /// <summary>
        /// Enables or disables rendering of ink.
        /// </summary>
        public virtual bool Enabled {
            get { return this.m_Core.Enabled; }
            set { this.m_Core.Enabled = value; }
        }

        /// <summary>
        /// Causes the <see cref="TranformableDynamicRenderer"/> to redraw the current stroke, if any.
        /// </summary>
        /// <remarks>
        /// This method should be invoked whenever the control onto which the
        /// <see cref="TranformableDynamicRenderer"/> is drawing is <see cref="Control.Invalidated">invalidated</see>.
        /// </remarks>
        public void Refresh() {
            this.m_Core.Refresh();
        }

        public void Refresh(Graphics g) {
            IntPtr hrgn = g.Clip.GetHrgn(g);
            IntPtr hdc = g.GetHdc();
            try {
                int saved = SaveDC(hdc);
                try {
                    SelectClipRgn(hdc, hrgn);

                    this.m_Core.Refresh(hdc);
                } finally {
                    RestoreDC(hdc, saved);
                }
            } finally {
                g.ReleaseHdc(hdc);
                g.Clip.ReleaseHrgn(hrgn);
            }
        }

        #endregion Core Wrapper


        #region IStylusSyncPlugin Members
        // Copied from the RealTimeStylus InkCollection example from
        // the Microsoft Tablet PC API Sample Applications collection.

        /// <summary>
        /// Occurs when the stylus touches the digitizer surface.
        /// </summary>
        /// <param name="sender">The real time stylus associated with the notification</param>
        /// <param name="data">The notification data</param>
        void IStylusSyncPlugin.StylusDown(RealTimeStylus sender, StylusDownData data) {
            // An inverted stylus is reserved for the eraser.
            if(data.Stylus.Inverted) return;

            this.m_Core.StylusDown(data.Stylus.Id, 0, data.GetData(),
                sender.GetTabletPropertyDescriptionCollection(data.Stylus.TabletContextId));
        }

        /// <summary>
        /// Occurs when the stylus moves on the digitizer surface.
        /// </summary>
        /// <param name="sender">The real time stylus associated with the notification</param>
        /// <param name="data">The notification data</param>
        void IStylusSyncPlugin.Packets(RealTimeStylus sender, PacketsData data) {
            this.m_Core.Packets(data.Stylus.Id, 0, data.GetData());
        }

        /// <summary>
        /// Occurs when the stylus leaves the digitizer surface.
        /// </summary>
        /// <param name="sender">The real time stylus associated with the notification</param>
        /// <param name="data">The notification data</param>
        void IStylusSyncPlugin.StylusUp(RealTimeStylus sender, StylusUpData data) {
            this.m_Core.StylusUp(data.Stylus.Id, 0, data.GetData());
        }

        /// <summary>
        /// Called when the current plugin or the ones previous in the list
        /// threw an exception.
        /// </summary>
        /// <param name="sender">The real time stylus</param>
        /// <param name="data">Error data</param>
        void IStylusSyncPlugin.Error(RealTimeStylus sender, ErrorData data) {
            Debug.Assert(false, null, "An error occurred while collecting ink.  Details:\n"
                + "DataId=" + data.DataId + "\n"
                + "Exception=" + data.InnerException + "\n"
                + "StackTrace=" + data.InnerException.StackTrace);
        }

        /// <summary>
        /// Defines the types of notifications the plugin is interested in.
        /// </summary>
        DataInterestMask IStylusSyncPlugin.DataInterest {
            get {
                return DataInterestMask.StylusDown
                    | DataInterestMask.Packets
                    | DataInterestMask.StylusUp
                    | DataInterestMask.Error
                    | DataInterestMask.CustomStylusDataAdded;
            }
        }

        /// <summary>
        /// Occurs when custom data is inserted into the <see cref="RealTimeStylus"/> queue by other plugins.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="data"></param>
        void IStylusSyncPlugin.CustomStylusDataAdded(RealTimeStylus sender, CustomStylusData data) {
            if(data.CustomDataId == StylusInputSelector.StylusData.Id) {
                // The new DrawingAttributes will take effect after the next StylusDown event.
                this.m_Core.DrawingAttributesChanged(
                    ((StylusInputSelector.StylusData) data.Data).DrawingAttributes);
            }
        }

        // The remaining interface methods are not used in this application.
        void IStylusSyncPlugin.RealTimeStylusDisabled(RealTimeStylus sender, RealTimeStylusDisabledData data) {}
        void IStylusSyncPlugin.RealTimeStylusEnabled(RealTimeStylus sender, RealTimeStylusEnabledData data) {}
        void IStylusSyncPlugin.StylusOutOfRange(RealTimeStylus sender, StylusOutOfRangeData data) {}
        void IStylusSyncPlugin.StylusInRange(RealTimeStylus sender, StylusInRangeData data) {}
        void IStylusSyncPlugin.StylusButtonDown(RealTimeStylus sender, StylusButtonDownData data) {}
        void IStylusSyncPlugin.StylusButtonUp(RealTimeStylus sender, StylusButtonUpData data) {}
        void IStylusSyncPlugin.SystemGesture(RealTimeStylus sender, SystemGestureData data) {}
        void IStylusSyncPlugin.InAirPackets(RealTimeStylus sender, InAirPacketsData data) {}
        void IStylusSyncPlugin.TabletAdded(RealTimeStylus sender, TabletAddedData data) {}
        void IStylusSyncPlugin.TabletRemoved(RealTimeStylus sender, TabletRemovedData data) {}

        #endregion IStylusSyncPlugin Members


        #region Core

        /// <summary>
        /// Implements the basic functionality of the <see cref="TransformableDynamicRenderer"/>.
        /// </summary>
        /// <remarks>
        /// This class (or a derivative such as <see cref="RealTimeInkAdapter"/>) should be used to render
        /// ink outside of the context of a <see cref="RealTimeStylus"/>.
        /// </remarks>
        public class Core : IDisposable {
            private readonly SlideDisplayModel m_SlideDisplay;
            private readonly Renderer m_Renderer;
            private readonly Ink m_Ink;
            private readonly Dictionary<int, int[]> m_PacketsTable;
            private readonly Dictionary<int, int> m_CurrentStrokeIdTable;
            private readonly Dictionary<int, DrawingAttributes> m_DrawingAttributesTable;
            private readonly Dictionary<int, TabletPropertyDescriptionCollection> m_TabletPropertiesTable;
            private readonly Dictionary<int, List<int>> m_CollectedPacketsTable;
            private DrawingAttributes m_DrawingAttributes;
            private bool m_Enabled;
            private bool m_Disposed;

            #region Construction & Destruction

            /// <summary>
            /// Constructs a new <see cref="Core"/> instance, which will
            /// render ink to the graphics device returned by the <see cref="SlideDisplayModel"/>.
            /// </summary>
            /// <remarks>
            /// Furthermore, the ink will be transformed using the
            /// <see cref="SlideDisplayModel.InkTransform"/>
            /// and clipped by <see cref="SlideDisplayModel.Bounds"/>.
            /// </remarks>
            public Core(SlideDisplayModel display) {
                this.m_SlideDisplay = display;
                this.m_Renderer = new Renderer();
                this.m_Ink = new Ink();
                this.m_DrawingAttributes = new DrawingAttributes();

                this.m_PacketsTable = new Dictionary<int,int[]>();
                this.m_DrawingAttributesTable = new Dictionary<int,DrawingAttributes>();
                this.m_TabletPropertiesTable = new Dictionary<int,TabletPropertyDescriptionCollection>();
                this.m_CollectedPacketsTable = new Dictionary<int, List<int>>();
                this.m_CurrentStrokeIdTable = new Dictionary<int, int>();

                this.m_SlideDisplay.Changed["InkTransform"].Add(new PropertyEventHandler(this.HandleInkTransformChanged));

                this.HandleInkTransformChanged(display, null);
            }

            ~Core() {
                this.Dispose(false);
            }

            public void Dispose() {
                this.Dispose(true);
                System.GC.SuppressFinalize(this);
            }

            protected virtual void Dispose(bool disposing) {
                if(this.m_Disposed) return;
                if(disposing) {
                    this.m_SlideDisplay.Changed["InkTransform"].Remove(new PropertyEventHandler(this.HandleInkTransformChanged));
                    this.m_Ink.Dispose();
                }
                this.m_Disposed = true;
            }

            #endregion Construction & Destruction

            protected Ink Ink {
                get { return this.m_Ink; }
            }

            protected Matrix ViewTransform {
                set { this.m_Renderer.SetViewTransform(value); }
            }

            public DrawingAttributes DrawingAttributes {
                get { return this.m_DrawingAttributes; }
                set { this.m_DrawingAttributes = value; }
            }

            /// <summary>
            /// Enables or disables rendering of ink.
            /// </summary>
            /// <remarks>
            /// This property takes effect only at the next <see cref="StylusDown"/> event.
            /// If the <see cref="Core"/> is enabled while a stroke is in progress,
            /// then the stroke will not be drawn.
            /// </remarks>
            public bool Enabled {
                get { return this.m_Enabled; }
                set {
                    using(Synchronizer.Lock(this.m_SlideDisplay.SyncRoot)) {
                        if(this.m_Enabled != value) {
                            // Clear the hashtables to avoid a memory leak from missing the StylusUp event.
                            this.m_PacketsTable.Clear();
                            this.m_DrawingAttributesTable.Clear();
                            this.m_TabletPropertiesTable.Clear();
                            this.m_CollectedPacketsTable.Clear();
                            this.m_CurrentStrokeIdTable.Clear();
                        }

                        this.m_Enabled = value;
                    }
                }
            }

            /// <summary>
            /// Notifies the <see cref="Core"/> that the <see cref="DrawingAttributes"/> which are to be
            /// used to render subsequent strokes have changed.
            /// </summary>
            /// <remarks>
            /// This property takes effect only at the next <see cref="StylusDown"/> event.
            /// If the <see cref="Core"/> is enabled while a stroke is in progress,
            /// then the stroke will continue to be rendered using the previous <see cref="DrawingAttributes"/>.
            /// </remarks>
            /// <param name="atts">The new <see cref="DrawingAttributes"/> to use to render ink.</param>
            public void DrawingAttributesChanged(DrawingAttributes atts) {
                // The new drawing attributes will take effect on the next StylusDown.
                this.DrawingAttributes = atts;
            }

            /// <summary>
            /// Notifies the <see cref="Core"/> of the start of a new stroke.
            /// </summary>
            /// <remarks>
            /// Note that some properties of the <see cref="Core"/> take effect only after a <see cref="StylusDown"/> event.
            /// </remarks>
            /// <param name="stylusId">The stylus which is being used to draw the stroke.</param>
            /// <param name="packets">The packet data describing the stroke.</param>
            /// <param name="tabletProperties">
            /// The <see cref="TabletPropertyDescriptionCollection"/>
            /// used to interpret the packet data, and also subsequent packet data from <see cref="Packets"/> and
            /// <see cref="StylusUp"/> events which have the same <paramref name="stylusId"/>.
            /// </param>
            public void StylusDown(int stylusId, int strokeId, int[] packets, TabletPropertyDescriptionCollection tabletProperties) {
                using(Synchronizer.Lock(this.m_SlideDisplay.SyncRoot)) {
                    if(!this.Enabled) return;

                    // Insert the data into a hashtable using the stylus id as a key.
                    this.m_PacketsTable[stylusId] = packets;

                    // Also store the current DrawingAttributes.  This is necessary to emulate the default
                    // DynamicRenderer which only updates its DrawingAttributes on StylusDown.
                    this.m_DrawingAttributesTable[stylusId] = this.DrawingAttributes;

                    // Store the packets in the collected packets list so we can retrieve them on Refresh().
                    List<int> collected = new List<int>(packets);
                    this.m_CollectedPacketsTable[stylusId] = collected;

                    // Store the strokeId so RenderIncomingPackets will know when to cancel an incomplete
                    // stroke if a StylusUp event is missed.
                    this.m_CurrentStrokeIdTable[stylusId] = strokeId;

                    // Finally store the tablet properties, which will be used to interpret the packets when rendering a stroke.
                    this.m_TabletPropertiesTable[stylusId] = tabletProperties;
                }
            }

            /// <summary>
            /// Notifies the <see cref="Core"/> of new data describing a stroke in progress.
            /// </summary>
            /// <remarks>
            /// The ink described by the packet data is rendered immediately, and also cached
            /// for use by <see cref="Refresh"/>.
            /// </remarks>
            /// <param name="stylusId">The stylus which is being used to draw the stroke.</param>
            /// <param name="packets">The packet data describing the stroke.</param>
            public void Packets(int stylusId, int strokeId, int[] packets) {
                this.RenderIncomingPackets(stylusId, strokeId, packets);
            }

            /// <summary>
            /// Notifies the <see cref="Core"/> of the end of the stroke.
            /// </summary>
            /// <remarks>
            /// The ink described by the packet data is rendered immediately.
            /// </remarks>
            /// <remarks>
            /// Any cached packet data generated by the <paramref name="stylusId"/> is cleared,
            /// so <see cref="Refresh"/> will no longer be able to redraw the completed stroke.
            /// A separate ink collection mechanism must be used to store and redraw completed strokes.
            /// </remarks>
            /// <param name="stylusId"></param>
            /// <param name="packets"></param>
            public void StylusUp(int stylusId, int strokeId, int[] packets) {
                using(Synchronizer.Lock(this.m_SlideDisplay.SyncRoot)) {
                    this.RenderIncomingPackets(stylusId, strokeId, packets);
                    this.CancelStroke(stylusId);
                }
            }

            private void CancelStroke(int stylusId) {
                // Remove the entries from the hash tables when they are no longer needed.
                this.m_PacketsTable.Remove(stylusId);
                this.m_CurrentStrokeIdTable.Remove(stylusId);
                this.m_DrawingAttributesTable.Remove(stylusId);
                this.m_TabletPropertiesTable.Remove(stylusId);
                this.m_CollectedPacketsTable.Remove(stylusId);
            }

            /// <summary>
            /// Used by <see cref="Packets"/> and <see cref="StylusUp"/> to render ink.
            /// </summary>
            private void RenderIncomingPackets(int stylusId, int strokeId, int[] packets) {
                using(Synchronizer.Lock(this.m_SlideDisplay.SyncRoot)) {
                    if(!this.Enabled) return;

                    //Allow rendering packets on disposed TransformableDynamicRenderer.
                    //if (this.m_Disposed) {
                    //    Debug.WriteLine("Warning: Ignoring request to RenderIncomingPackets on disposed TransformableDynamicRenderer.");
                    //    return;
                    //}
                    // Verify that the stroke we're about to render matches the StrokeId
                    // from the most recent StylusDown event.  (If not, then something 
                    // probably got lost over the network.)
                    int currentStrokeId;
                    if (!this.m_CurrentStrokeIdTable.TryGetValue(stylusId, out currentStrokeId) || currentStrokeId != strokeId) {
                        // First cancel the previous stroke to avoid
                        // drawing a bogus connecting line between the endpoints.
                        this.CancelStroke(stylusId);
                        // Then attempt to start a new stroke.  Since we didn't get the 
                        // StylusDown event, we'll have to re-use the old TabletPropertyDescriptionCollection.
                        // (Theoretically this might have changed between strokes, but that's unlikely.)
                        // But if there was no previous stroke, and therefore no TabletPropertyDescriptionCollection
                        // we can use to render the packets, we'll have to give up.
                        TabletPropertyDescriptionCollection props;
                        if(!this.m_TabletPropertiesTable.TryGetValue(stylusId, out props))
                            return; // Give up!
                        this.StylusDown(stylusId, strokeId, new int[] { }, props);
                    }

                    // At this point we're guaranteed to have executed a (real or simulated)
                    // StylusDown event, so the entries in all the other dictionaries must exist.

                    // Get the DrawingAttributes which were in effect on StylusDown.
                    DrawingAttributes atts = this.m_DrawingAttributesTable[stylusId];

                    // Discard the stroke if we have no DrawingAttributes.
                    if(atts == null) return;

                    // Get the tablet description which the renderer will use to interpret the data.
                    TabletPropertyDescriptionCollection tabletProperties = this.m_TabletPropertiesTable[stylusId];
                    Debug.Assert(tabletProperties != null);

                    // Retrieve the packet array from the hashtable using the cursor id as a key.
                    int[] previousData = this.m_PacketsTable[stylusId];
                    Debug.Assert(previousData != null);

                    // Store the new data so that the next rendering job will only have to "connect the dots".
                    this.m_PacketsTable[stylusId] = packets;

                    // Also add the packets to the collected packets list.
                    List<int> collected = this.m_CollectedPacketsTable[stylusId];
                    collected.AddRange(packets);

                    //Calculate the distance between the last point of previous stroke, and first point of current stroke.
                    //If the distance is smaller than distanceThreshold, then draw both previous and current strokes.
                    bool combinedMode=true;
                    double distanceThreshold = 2500;
                    Stroke previousStroke = this.Ink.CreateStroke(previousData, tabletProperties);
                    Stroke currentStroke = this.Ink.CreateStroke(packets, tabletProperties);
                    Point lastPointInPrevious = previousStroke.GetPoint(previousStroke.GetPoints().Length - 1);
                    Point firstPointInCurrent = currentStroke.GetPoint(0);
                    int x = lastPointInPrevious.X - firstPointInCurrent.X;
                    int y = lastPointInPrevious.Y - firstPointInCurrent.Y;
                    if (Math.Sqrt(x * x + y * y) > distanceThreshold)combinedMode = false;
                        
                    // Assemble the completed information we'll need to create the mini-stroke.
                    int[] combinedPackets = new int[previousData.Length + packets.Length];
                    previousData.CopyTo(combinedPackets, 0);
                    packets.CopyTo(combinedPackets, previousData.Length);

                    // Now that we have the data, we're ready to create the temporary stroke
                    // which we will immediately render.
                    Stroke stroke = null;
                    if(combinedMode) stroke=this.Ink.CreateStroke(combinedPackets, tabletProperties);
                    else stroke = this.Ink.CreateStroke(packets, tabletProperties);
                    stroke.DrawingAttributes = atts;

                    // Render the stroke onto the canvas.
                    using (Graphics graphics = this.m_SlideDisplay.CreateGraphics()) {
                        IntPtr hdc = graphics.GetHdc();
                        try {
                            int saved = SaveDC(hdc);
                            try {
                                // Copy the clip region from the slide viewer to the DC.
                                Rectangle bounds = this.m_SlideDisplay.Bounds;
                                IntersectClipRect(hdc, bounds.Left, bounds.Top, bounds.Right, bounds.Bottom);

                                // Draw the ink!
                                this.m_Renderer.Draw(hdc, stroke);
                            } finally {
                                RestoreDC(hdc, saved);
                            }
                        } finally {
                            graphics.ReleaseHdc();
                        }
                    }

                    // Immediately delete the stroke, since there's no easy way to save it and modify it for the next data.
                    this.Ink.DeleteStroke(stroke);
                }
            }


            /// <summary>
            /// Re-renders all strokes which are currently being drawn.
            /// </summary>
            /// <remarks>
            /// This method should be invoked whenever the control onto which the
            /// <see cref="Core"/> is drawing is <see cref="Control.Invalidated">invalidated</see>.
            /// </remarks>
            /// <remarks>
            /// This method re-renders only the ink packets which have been received since the last <c>StylusDown</c> event
            /// but before a <c>StylusUp</c> event.  Once <c>StylusUp</c> is received, the packets are discarded and must
            /// be rendered elsewhere.
            /// </remarks>
            public void Refresh() {
                using (Graphics graphics = this.m_SlideDisplay.CreateGraphics()) {
                    IntPtr hdc = graphics.GetHdc();
                    try {
                        int saved = SaveDC(hdc);
                        try {
                            using (Synchronizer.Lock(this.m_SlideDisplay.SyncRoot)) {
                                // Copy the clip region from the slide viewer to the DC.
                                Rectangle bounds = this.m_SlideDisplay.Bounds;
                                IntersectClipRect(hdc, bounds.Left, bounds.Top, bounds.Right, bounds.Bottom);

                                this.Refresh(hdc);
                            }
                        } finally {
                            RestoreDC(hdc, saved);
                        }
                    } finally {
                        graphics.ReleaseHdc();
                    }
                }
            }

            /// <summary>
            /// Re-renders all strokes which are currently being drawn.
            /// </summary>
            /// <remarks>
            /// This method should be invoked whenever the control onto which the
            /// <see cref="Core"/> is drawing is <see cref="Control.Invalidated">invalidated</see>.
            /// </remarks>
            /// <remarks>
            /// This method re-renders only the ink packets which have been received since the last <c>StylusDown</c> event
            /// but before a <c>StylusUp</c> event.  Once <c>StylusUp</c> is received, the packets are discarded and must
            /// be rendered elsewhere.
            /// </remarks>
            protected internal void Refresh(IntPtr hdc) {
                using(Synchronizer.Lock(this.m_SlideDisplay.SyncRoot)) {
                    foreach(int stylusId in this.m_CollectedPacketsTable.Keys) {
                        // Get the DrawingAttributes which were in effect on StylusDown.
                        DrawingAttributes atts = ((DrawingAttributes) this.m_DrawingAttributesTable[stylusId]);

                        // Discard the stroke if we have no DrawingAttributes.
                        if(atts == null) continue;

                        // Get the tablet description which the renderer will use to interpret the data.
                        TabletPropertyDescriptionCollection tabletProperties =
                            ((TabletPropertyDescriptionCollection) this.m_TabletPropertiesTable[stylusId]);
                        Debug.Assert(tabletProperties != null);

                        // Also add the packets to the collected packets list.
                        List<int> collected = this.m_CollectedPacketsTable[stylusId];
                        int[] packets = collected.ToArray();

                        // Create the temporary stroke which we will immediately render.
                        Stroke stroke = this.Ink.CreateStroke(packets, tabletProperties);
                        stroke.DrawingAttributes = atts;

                        // Now draw the stroke.
                        this.m_Renderer.Draw(hdc, stroke);

                        // Immediately delete the stroke, since there's no easy way to save it and modify it for the next data.
                        this.Ink.DeleteStroke(stroke);
                    }
                }
            }

            private void HandleInkTransformChanged(object sender, PropertyEventArgs args) {
                using(Synchronizer.Lock(this.m_SlideDisplay.SyncRoot)) {
                    this.m_Renderer.SetViewTransform(this.m_SlideDisplay.InkTransform);
                }
            }


            /// <summary>
            /// A convenience class which allows a <see cref="Core"/> instance to "tap&nbsp;in" to events
            /// generated by a <see cref="RealTimeInkSheetModel"/>.
            /// </summary>
            public class RealTimeInkAdapter : Core {
                private readonly EventQueue.PropertyEventDispatcher m_CurrentDrawingAttributesChangedDispatcher;
                private new bool m_Disposed;
                private RealTimeInkSheetModel m_RTI;

                /// <summary>
                /// Constructs a new <see cref="RealTimeInkAdapater"/> instance, which will render ink
                /// generated received from the <see cref="RealTimeInkSheetModel"/> onto the graphics device
                /// specified by the <see cref="SlideDisplayModel"/>
                /// </summary>
                public RealTimeInkAdapter(SlideDisplayModel display, RealTimeInkSheetModel rti) : base(display) {
                    this.m_RTI = rti;

                    this.m_CurrentDrawingAttributesChangedDispatcher = new EventQueue.PropertyEventDispatcher(this.m_SlideDisplay.EventQueue, new PropertyEventHandler(this.HandleCurrentDrawingAttributesChanged));
                    this.m_RTI.Changed["CurrentDrawingAttributes"].Add(this.m_CurrentDrawingAttributesChangedDispatcher.Dispatcher);
                    this.m_RTI.StylusDown += new RealTimeInkSheetModel.StylusDownEventHandler(this.HandleStylusDown);
                    this.m_RTI.StylusUp += new RealTimeInkSheetModel.StylusUpEventHandler(this.HandleStylusUp);
                    this.m_RTI.Packets += new RealTimeInkSheetModel.PacketsEventHandler(this.HandlePackets);

                    this.m_CurrentDrawingAttributesChangedDispatcher.Dispatcher(this.m_RTI, null);
                }

                protected override void Dispose(bool disposing) {
                    if(this.m_Disposed) return;
                    this.m_Disposed = true;
                    try {
                        if(disposing) {
                            this.m_RTI.Changed["CurrentDrawingAttributes"].Remove(this.m_CurrentDrawingAttributesChangedDispatcher.Dispatcher);
                            this.m_RTI.StylusDown -= new RealTimeInkSheetModel.StylusDownEventHandler(this.HandleStylusDown);
                            this.m_RTI.StylusUp -= new RealTimeInkSheetModel.StylusUpEventHandler(this.HandleStylusUp);
                            this.m_RTI.Packets -= new RealTimeInkSheetModel.PacketsEventHandler(this.HandlePackets);
                        }
                    } finally {
                        base.Dispose(disposing);
                    }
                }

                private void HandleCurrentDrawingAttributesChanged(object sender, PropertyEventArgs args) {
                    using(Synchronizer.Lock(this.m_SlideDisplay.SyncRoot)) {
                        using(Synchronizer.Lock(this.m_RTI.SyncRoot)) {
                            this.DrawingAttributes = this.m_RTI.CurrentDrawingAttributes;
                        }
                    }
                }

                private void HandleStylusDown(object sender, int stylusId, int strokeId, int[] packets, TabletPropertyDescriptionCollection tabletProperties) {
                    this.StylusDown(stylusId, strokeId, packets, tabletProperties);
                }

                private void HandleStylusUp(object sender, int stylusId, int strokeId, int[] packets) {
                    this.StylusUp(stylusId, strokeId, packets);
                }

                private void HandlePackets(object sender, int stylusId, int strokeId, int[] packets) {
                    this.Packets(stylusId, strokeId, packets);
                }
            }
        }

        #endregion Core

        [DllImport("gdi32.dll")]
        private static extern int SelectClipRgn(IntPtr hdc, IntPtr hrgn);
        [DllImport("gdi32.dll")]
        private static extern int IntersectClipRect(IntPtr hdc, int left, int top, int right, int bottom);
        [DllImport("gdi32.dll")]
        private static extern bool RestoreDC(IntPtr hdc, int nSavedDC);
        [DllImport("gdi32.dll")]
        private static extern int SaveDC(IntPtr hdc);
        [DllImport("user32.dll")]
        private static extern IntPtr GetDC(IntPtr hwnd);
        [DllImport("user32.dll")]
        private static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc);
    }

    /// <summary>
    /// A convenience class which invokes <see cref="Refresh"/> whenever it receives a
    /// <see cref="Control.Paint"/> event from the specified <see cref="Control"/>, and
    /// also which sets <see cref="Enabled"/> to <c>false</c> whenever the specified
    /// <see cref="SlideDisplayModel"/>'s <see cref="SlideDisplayModel.Slide"><c>Slide</c></see>
    /// property is <c>null</c>.
    /// </summary>
    public class TransformableDynamicRendererLinkedToDisplay : TransformableDynamicRenderer {
        private readonly SlideDisplayModel m_SlideDisplay;
        private readonly Control m_Control;
        private bool m_Enabled;
        private bool m_Disposed;

        /// <summary>
        /// Creates a new instance of <see cref="TransformableDynamicRendererLinkedToDisplay"/>, which
        /// listens to the <see cref="Control.Paint"/> event and <see cref="SlideDisplayModel.Slide"/> property.
        /// </summary>
        public TransformableDynamicRendererLinkedToDisplay(Control control, SlideDisplayModel display) : base(display) {
            this.m_Control = control;
            this.m_SlideDisplay = display;

            this.m_SlideDisplay.Changed["Slide"].Add(new PropertyEventHandler(this.HandleSlideChanged));
            this.m_Control.Paint += new PaintEventHandler(this.HandleViewerPaint);

            this.HandleSlideChanged(this.m_SlideDisplay, null);
        }

        protected override void Dispose(bool disposing) {
            if(this.m_Disposed) return;
            try {
                if(disposing) {
                    this.m_SlideDisplay.Changed["Slide"].Remove(new PropertyEventHandler(this.HandleSlideChanged));
                    this.m_Control.Paint -= new PaintEventHandler(this.HandleViewerPaint);
                }
            } finally {
                base.Dispose(disposing);
            }
            this.m_Disposed = true;
        }

        /// <summary>
        /// Enables or disables rendering of ink.
        /// </summary>
        /// <remarks>
        /// Even when <see cref="Enabled"/> is <c>true</c>, ink will <em>not</em> be rendered
        /// if the <see cref="SlideDisplayModel.Slide"/> is <c>null</c>.
        /// </remarks>
        public override bool Enabled {
            get {
                return this.m_Enabled;
            }

            set {
                using(Synchronizer.Lock(this.m_SlideDisplay.SyncRoot)) {
                    this.m_Enabled = value;
                    base.Enabled = (value && (this.m_SlideDisplay.Slide != null));
                }
            }
        }

        private void HandleSlideChanged(object sender, PropertyEventArgs args) {
            // Refresh the base's enabled state, which depends both on this subclass's
            // m_Enabled and whether the viewer has a non-null Slide.
            using(Synchronizer.Lock(this.m_SlideDisplay.SyncRoot)) {
                this.Enabled = this.Enabled;
            }
        }

        private void HandleViewerPaint(object sender, PaintEventArgs args) {
            this.Refresh(args.Graphics);
        }
    }
}
