/// <summary>
        /// Calculates ambient occlusion at a point on a surface.
        /// </summary>
        /// <param name="surface">Information about surface point</param>
        /// <param name="geometry">The geometry to be raytraced (both occlusion caster and occlusion receiver)</param>
        /// <param name="random">Random number generator to use</param>
        /// <returns>Fraction of non-occlusion at surface point (between 0 and 1): 0 = surface point fully occluded by neighbour surfaces; 1 = surface point not occluded at all</returns>
        private double CalcAmbientOcclusion(IntersectionInfo surface, Raytrace.IRayIntersectable geometry, RenderContext context)
        {
            Contract.Ensures(0 <= Contract.Result <double>() && Contract.Result <double>() <= 1);

            var random   = context.RNG;
            var rayStart = surface.pos + surface.normal * ambientOcclusionProbeOffset;

            Vector avgEscapedRayDir = new Vector();
            int    rayEscapeCount   = 0;

            for (int i = 0; i < ambientOcclusionQuality; i++)
            {
                // Pick a random direction within the hemisphere around the surface normal
                // TODO: works for external surfaces, but some self-intersection on interior surfaces
                var rayDir = new Vector(random.NextDouble() * 2 - 1, random.NextDouble() * 2 - 1, random.NextDouble() * 2 - 1);
                //rayDir.Normalise();
                if (rayDir.DotProduct(surface.normal) < 0)
                {
                    rayDir = -rayDir;
                }

                // Pick random directions until we find one roughly in the same direction as the surface normal
                //Vector rayDir;
                //double cosOfAngle;
                //do
                //{
                //    rayDir = new Vector(random.NextDouble() * 2 - 1, random.NextDouble() * 2 - 1, random.NextDouble() * 2 - 1);
                //    rayDir.Normalise();
                //    cosOfAngle = rayDir.DotProduct(surfaceInfo.normal);

                //    // force ray to be in the hemisphere around the surface normal
                //} while (cosOfAngle < 0.0);

                // Fire off ray to check for nearby surface in chosen direction
                // TODO: might be more efficient if ray tracing stopped after a short distance from ray origin
                Raytrace.IntersectionInfo shadowInfo = geometry.IntersectRay(rayStart, rayDir, context);
                if (shadowInfo == null || shadowInfo.rayFrac > ambientOcclusionProbeDist)
                {
                    // This ray did not hit a nearby surface
                    rayEscapeCount++;
                    avgEscapedRayDir += rayDir;
                }
            }


            // visualise direction of unobstructed space around surface point
            //avgEscapedRayDir.Normalise();
            //var v = new Vector(avgEscapedRayDir.x + 1, avgEscapedRayDir.y + 1, avgEscapedRayDir.z + 1);
            //v *= 128;
            //return Surface.PackRgb((byte)v.x, (byte)v.y, (byte)v.z);

            //avgEscapedRayDir.Normalise();
            //avgEscapedRayDir *= 255;
            //return Surface.PackRgb((byte)avgEscapedRayDir.x, (byte)avgEscapedRayDir.y, (byte)avgEscapedRayDir.z);


            return((double)rayEscapeCount / (double)ambientOcclusionQuality);
        }
        //public void CalcAllAmbientOcclusion()
        //{
        //    // TODO: write brute force algorithm: for every cell in 3D cube, find intersecting triangles and calc AO for these? Or just find nearest triangle to each cell center?
        //}

        /// <summary>
        /// Calculates and caches ambient occlusion at a point on a surface.
        /// Multithread safe.
        /// </summary>
        /// <param name="surface">Information about surface point</param>
        /// <param name="geometry">The geometry to be raytraced (both occlusion caster and occlusion receiver)</param>
        /// <param name="random">Random number generator to use. Set to <value>NULL</value> to use the default random number generator</param>
        /// <returns>Fraction of non-occlusion at surface point (between 1 and 255): 1 = surface point fully occluded by neighbour surfaces; 255 = surface point not occluded at all</returns>
        public T CacheAmbientOcclusion(IntersectionInfo surface, Raytrace.IRayIntersectable geometry, RenderContext context)
        {
            Contract.Requires(surface != null);

            // TODO: Sometimes a surface point coordinate is very slightly outside the unit cube (i.e. Z coordinate of 0.50000000001)
            const double maxExtent = 0.5; // TODO: extent seems too large on Couch model...
            const double scale     = 0.5 / maxExtent;

            // So we clamp coordinates to the unit cube
            surface.pos.x = Math.Min(Math.Max(-0.5, surface.pos.x), 0.5);
            surface.pos.y = Math.Min(Math.Max(-0.5, surface.pos.y), 0.5);
            surface.pos.z = Math.Min(Math.Max(-0.5, surface.pos.z), 0.5);

/*
 *          Assert.IsTrue(-maxExtent <= surface.pos.x && surface.pos.x <= maxExtent, "Surface point is outside unit cube");
 *          Assert.IsTrue(-maxExtent <= surface.pos.y && surface.pos.y <= maxExtent, "Surface point is outside unit cube");
 *          Assert.IsTrue(-maxExtent <= surface.pos.z && surface.pos.z <= maxExtent, "Surface point is outside unit cube");
 */
            var cacheIndex = (int)((surface.pos.x * scale + 0.5) * (cacheSize - 1)) * cacheSize * cacheSize +
                             (int)((surface.pos.y * scale + 0.5) * (cacheSize - 1)) * cacheSize +
                             (int)((surface.pos.z * scale + 0.5) * (cacheSize - 1));

            // fetch cache entry, but if missing then calc AO factor
            // TODO: probably not multithread safe!
            // TODO: tri-linearly interpolate AO factor from eight neighbouring cache entries for smoother results!

            // Cheap thread lock - prevent other threads recalculating this value, while it is calculated by this thread
            // TODO: cannot be used with byte values, but could be used with ints
            //if(0 == Interlocked.CompareExchange<byte>(ref cacheData[cacheIndex], (byte)1, (byte)0))

            if (!EnableCache)
            {
                // calc shading intensity based on percentage AO shadowing
                double unoccludedFactor = CalcAmbientOcclusion(surface, geometry, context);
                // scale to range [1, 255]. We avoid zero as this is a sentinel value indicating empty cache entry.
                return((T)(unoccludedFactor * 254 + 1));
            }

            // Double-check locking pattern
            // TODO: rare IndexOutOfRangeException here
            T lightByte = cacheData[cacheIndex];

            if (EmptyCacheEntry == lightByte)
            {
                lock (calcLock)
                {
                    // another thread may have got the lock first, and already calculated this value
                    if (EmptyCacheEntry == cacheData[cacheIndex])
                    {
                        // calc shading intensity based on percentage AO shadowing
                        double unoccludedFactor = CalcAmbientOcclusion(surface, geometry, context);
                        // scale to range [1, 255]. We avoid zero as this is a sentinel value indicating empty cache entry.
                        lightByte             = (T)(unoccludedFactor * 254 + 1);
                        cacheData[cacheIndex] = lightByte;

                        // Time to persist cache data to file in isolated storage?
                        numCalcsUntilNextPersist--;
                        if (numCalcsUntilNextPersist <= 0)
                        {
                            // TODO: lock may be held for long time!
                            SaveCacheToDisk(cacheFilePath, cacheData);
                            numCalcsUntilNextPersist = numCalcsBetweenPersists;
                        }
                    }
                }
            }

            return(lightByte);
        }