/*

Copyright 2012 Nathan Dinsmore

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Last changed 2013-04-03 by Jens Moenig (disabled text area overlay)

*/

var sb = (function (sb) {
    'use strict';

    function extend(o, p) {
        var key;
        for (key in p) if (p.hasOwnProperty(key)) {
            o[key] = p[key];
        }
    }
    sb.$extend = extend;

    extend((sb.Ref = function (id) {
        this.id = id;
    }).prototype, {
        isRef: true
    });

    extend((sb.Dictionary = function (keys, values) {
        this.keys = keys;
        this.values = values;
    }).prototype, {
        get: function (key) {
            return this.values[this.keys.indexOf(key)];
        },
        set: function (key, value) {
            var i = this.keys.indexOf(key);
            if (i === -1) {
                this.keys.push(key);
                this.values.push(value);
            } else {
                this.values[i] = value;
            }
        }
    });

    sb.Color = function (rgb, a) {
        this.r = (rgb / 0x100000 | 0) % 0x400 / (0x400 - 1);
        this.g = (rgb / 0x400 | 0) % 0x400 / (0x400 - 1);
        this.b = (rgb % 0x400) / (0x400 - 1);
        this.a = a / (0x100 - 1);
    };
    sb.Point = function (x, y) {
        this.x = x;
        this.y = y;
    };
    sb.Rectangle = function (x, y, x2, y2) {
        this.origin = new sb.Point(x, y);
        this.corner = new sb.Point(x2, y2);
    };

    sb.indexedColors = [
        [1, 1, 1, 1],
        [0, 0, 0, 1],
        [1, 1, 1, 1],
        [.5, .5, .5, 1],
        [1, 0, 0, 1],
        [0, 1, 0, 1],
        [0, 0, 1, 1],
        [0, 1, 1, 1],
        [1, 1, 0, 1],
        [1, 0, 1, 1],
        [.125, .125, .125, 1],
        [.25, .25, .25, 1],
        [.375, .375, .375, 1],
        [.625, .625, .625, 1],
        [.75, .75, .75, 1],
        [.875, .875, .875, 1]
    ];
    (function () {
        var i, r, g, b, grayVal;
        for (i = 1; i <= 31; ++i) {
            if (i % 4 != 0) {
                grayVal = i / 32;
                sb.indexedColors[i + 15] = [grayVal, grayVal, grayVal, 1];
            }
        }
        for (r = 0; r < 6; ++r) {
            for (g = 0; g < 6; ++g) {
                for (b = 0; b < 6; ++b) {
                    i = 40 + 36 * r + 6 * b + g;
                    sb.indexedColors[i] = [r / 5, g / 5, b / 5, 1];
                }
            }
        }
    })();

    sb.colorDepthLengths = {
        1: 1,
        2: 2,
        4: 4,
        8: 8,
        15: 5,
        16: 5,
        12: 4,
        9: 3
    };
    extend((sb.ColorStream = function (bitmap, depth) {
        var i = 0,
            l = bitmap.length,
            b = this.bits = [],
            n;
        this.length = sb.colorDepthLengths[this.depth = depth];
        this.position = 0;
        while (i < l) {
            n = bitmap[i++];
            b.push(n / 0x80 & 1);
            b.push(n / 0x40 & 1);
            b.push(n / 0x20 & 1);
            b.push(n / 0x10 & 1);
            b.push(n / 0x8 & 1);
            b.push(n / 0x4 & 1);
            b.push(n / 0x2 & 1);
            b.push(n & 1);
        }
    }).prototype, {
        read: function () {
            var i = this.length,
                b = this.bits,
                n = 0;
            while (i--) {
                n = n * 2 + b[this.position++];
            }
            return n;
        }
    });

    extend((sb.ColorStreamIndexed = function (bitmap, depth) {
        sb.ColorStream.call(this, bitmap, depth);
    }).prototype = Object.create(sb.ColorStream.prototype), {
        next: function (b) {
            var c = sb.indexedColors[this.read()];
            if (!c) return c;
            b[0] = c[0];
            b[1] = c[1];
            b[2] = c[2];
            b[3] = c[3];
        }
    });

    extend((sb.ColorStreamRGB = function (bitmap, depth) {
        sb.ColorStream.call(this, bitmap, depth);
        this.max = Math.pow(2, this.length) - 1;
    }).prototype = Object.create(sb.ColorStream.prototype), {
        next: function (b) {
            var c;
            switch (this.depth) {
            case 16:
                ++this.position;
                c = [this.read() / this.max, this.read() / this.max, this.read() / this.max, 1];
                if (c[0] + c[1] + c[2] === 0) {
                    return b[0] = b[1] = b[2] = b[3] = 0;
                }
                b[0] = c[0];
                b[1] = c[1];
                b[2] = c[2];
                b[3] = c[3];
                return;
            case 12:
                this.position += 3;
                break;
            }
            b[0] = this.read() / this.max;
            b[1] = this.read() / this.max;
            b[2] = this.read() / this.max;
            b[3] = 1;
        }
    });

    extend((sb.ColorStream32 = function (bitmap, depth) {
        this.bytes = bitmap;
        if (depth === 32) {
            this.next = this.nextAlpha;
        }
        this.position = 0;
    }).prototype, {
        next: function (b) {
            b[0] = this.read() / 255;
            b[1] = this.read() / 255;
            b[2] = this.read() / 255;
            b[3] = 1;
        },
        nextAlpha: function (b) {
            var a = this.read() / 255;
            b[0] = this.read() / 255;
            b[1] = this.read() / 255;
            b[2] = this.read() / 255
            b[3] = a;
        },
        read: function () {
            return this.bytes[this.position++];
        }
    });

    sb.colorStreams = {
        1: sb.ColorStreamIndexed,
        2: sb.ColorStreamIndexed,
        4: sb.ColorStreamIndexed,
        8: sb.ColorStreamIndexed,
        15: sb.ColorStreamRGB,
        16: sb.ColorStreamRGB,
        32: sb.ColorStream32,
        24: sb.ColorStream32,
        12: sb.ColorStreamRGB,
        9: sb.ColorStreamRGB
    };

    sb.getColorStream = function (bitmap, depth) {
        return new sb.colorStreams[depth](bitmap, depth);
    };

    extend((sb.Form = function (w, h, d, o, b) {
        this.width = w;
        this.height = h;
        this.depth = d;
        this.offset = o;
        this.bits = b;
    }).prototype, {
        init: function (bm) {
            if (this.bits.isBitmap) {
                this.bitmap = this.bits;
            } else {
                this.decompress(bm);
            }
            this.image = document.createElement('canvas');
            this.image.width = this.width;
            this.image.height = this.height;
        },
        decompress: function (bm) {
            var b = this.bits,
                p = 0,
                q = 0,
                r = !!bm,
                length = (i = b[p++], i <= 223 ? i : i < 255 ? (i - 224) * 256 + b[p++] : (b[p++] << 24) + (b[p++] << 16) + (b[p++] << 8) + b[p++]),
                bm, i, n, d, e, f, g;
            // stream = new sb.Reader().on(this.bits),
            this.bitmap = bm || (bm = []);
            while (p < b.length) {
                i = b[p++];
                if (i > 223) {
                    i = i < 255 ? (i - 224) * 256 + b[p++] : (b[p++] << 24) + (b[p++] << 16) + (b[p++] << 8) + b[p++];
                }
                n = i >> 2;
                switch (i & 3) {
                case 1:
                    d = b[p++];
                    n *= 4;
                    while (n--) {
                        bm[q++] = d;
                    }
                    break;
                case 2:
                    // d = stream.readBytes(4);
                    d = b[p++];
                    e = b[p++];
                    f = b[p++];
                    g = b[p++];
                    while (n--) {
                        if (r) {
                            bm[q++] = e;
                            bm[q++] = f;
                            bm[q++] = g;
                            bm[q++] = d;
                        } else {
                            bm[q++] = d;
                            bm[q++] = e;
                            bm[q++] = f;
                            bm[q++] = g;
                        }
                    }
                    break;
                case 3:
                    while (n--) {
                        if (r) {
                            d = b[p++];
                            bm[q++] = b[p++];
                            bm[q++] = b[p++];
                            bm[q++] = b[p++];
                            bm[q++] = d;
                        } else {
                            bm[q++] = b[p++];
                            bm[q++] = b[p++];
                            bm[q++] = b[p++];
                            bm[q++] = b[p++];
                        }
                    }
                    // bm.push.apply(bm, stream.readBytes(n * 4));
                    break;
                }
            }
        },
        load: function () {
            var w = this.width,
                h = this.height,
                x, y, i, imageData, data, context, colors, color, b;
            if (this.depth === 32) {
                this.image = document.createElement('canvas');
                this.image.width = this.width;
                this.image.height = this.height;
                data = (imageData = (context = this.image.getContext('2d')).createImageData(this.width, this.height)).data;
                this.decompress(data);
                context.putImageData(imageData, 0, 0);
                return;
            }
            color = [0, 0, 0, 0];
            if (this.depth === 16) {
                w = this.width += 1;
            }
            this.init();
            colors = sb.getColorStream(this.bitmap, this.depth);
            data = (imageData = (context = this.image.getContext('2d')).createImageData(this.width, this.height)).data;
            for (x = 0; x < w; ++x) {
                for (y = 0; y < h; ++y) {
                    colors.next(color);
                    if (!color) continue;
                    i = (x * h + y) * 4;
                    data[i] = color[0] * 255;
                    data[i + 1] = color[1] * 255;
                    data[i + 2] = color[2] * 255;
                    data[i + 3] = color[3] * 255;
                }
            }
            context.putImageData(imageData, 0, 0);
        }
    });

    extend((sb.ColorForm = function (w, h, d, o, b, c) {
        this.width = w;
        this.height = h;
        this.depth = d;
        this.offset = o;
        this.bits = b;
        this.colors = c;
    }).prototype = Object.create(sb.Form.prototype), {
        load: function () {
            var w = this.width,
                h = this.height,
                colors = this.colors,
                bits, x, y, i, imageData, data, context, color;
            this.init();
            data = (imageData = (context = this.image.getContext('2d')).createImageData(this.width, this.height)).data;
            bits = this.bitmap;
            for (x = 0; x < w; ++x) {
                for (y = 0; y < h; ++y) {
                    color = colors[bits[x * h + y]];
                    if (!color) continue;
                    i = (x * h + y) * 4;
                    data[i] = color.r * 255;
                    data[i + 1] = color.g * 255;
                    data[i + 2] = color.b * 255;
                    data[i + 3] = color.a * 255;
                }
            }
            context.putImageData(imageData, 0, 0);
        }
    });

    sb.fields = {};
    sb.classIDs = {};
    sb.classNames = {};
    sb.addFields = function (id, name, base, fields) {
        sb.classIDs[name] = id;
        sb.classNames[id] = name;
        fields = fields.length ? fields.split(',') : [];
        if (base) {
            base = sb.fields[sb.classIDs[base]];
            if (!base) throw new Error('Initialization error');
            fields = base.concat(fields);
        }
        sb.fields[id] = fields;
    };

	sb.addFields(100, 'Morph', '', 'bounds,owner,submorphs,color,flags,properties');
	sb.addFields(101, 'BorderedMorph', 'Morph', 'borderWidth,borderColor');
	sb.addFields(102, 'RectangleMorph', 'BorderedMorph', '');
	sb.addFields(103, 'EllipseMorph', 'BorderedMorph', '');
	sb.addFields(104, 'AlignmentMorph', 'RectangleMorph', 'orientation,centering,hResizing,vResizing,inset');
	sb.addFields(105, 'StringMorph', 'Morph', 'fontSpec,emphasis,contents');

	sb.addFields(-1, 'Slider', 'BorderedMorph', 'slider,value,setValueSelector,sliderShadow,sliderColor,descending,model');
	sb.addFields(-2, 'AbstractSound', '', '');
	sb.addFields(-3, 'ScriptableScratchMorph', 'Morph', 'objName,vars,blocksBin,customBlocks,isClone,media,costume');
	sb.addFields(-4, 'ArgMorph', 'BorderedMorph', 'labelMorph');
	sb.addFields(-5, 'PasteUpMorph', 'BorderedMorph', '');
	sb.addFields(-6, 'ScratchMedia', '', 'mediaName');
	sb.addFields(-7, 'ScrollFrameMorph', 'BorderedMorph', '');

	sb.addFields(106, 'UpdatingStringMorph', 'StringMorph', 'format,target,getSelector,putSelector,parameter,floatPrecision,growable,stepTime');
	sb.addFields(107, 'SimpleSliderMorph', 'Slider', 'target,arguments,minVal,maxVal,truncate,sliderThickness');
	sb.addFields(108, 'SimpleButtonMorph', 'RectangleMorph', 'target,actionSelector,arguments,actWhen');
	sb.addFields(109, 'SampledSound', 'AbstractSound', 'envelopes,scaledVol,initialCount,samples,originalSamplingRate,samplesSize,scaledIncrement,scaledInitialIndex');
	sb.addFields(110, 'ImageMorph', 'Morph', 'form,transparency');
	sb.addFields(111, 'SketchMorph', 'Morph', 'originalForm,rotationCenter,rotationDegrees,rotationStyle,scalePoint,offsetWhenRotated');
	sb.addFields(123, 'SensorBoardMorph', 'Morph', 'portNum');
	sb.addFields(124, 'ScratchSpriteMorph', 'ScriptableScratchMorph', 'visibility,scalePoint,rotationDegrees,rotationStyle,volume,tempoBPM,draggable,sceneStates,lists,virtualScale,ownerSprite,subsprites,rotateWithOwner,refPos,prototype,deletedAttributes');
	sb.addFields(125, 'ScratchStageMorph', 'ScriptableScratchMorph', 'zoom,hPan,vPan,obsoleteSavedState,sprites,volume,tempoBPM,sceneStates,lists');
	sb.addFields(140, 'ChoiceArgMorph', 'ArgMorph', 'isBoolean,options,choice,getOptionsSelector');
	sb.addFields(141, 'ColorArgMorph', 'ArgMorph', '');
	sb.addFields(142, 'ExpressionArgMorph', 'ArgMorph', 'isNumber');
	sb.addFields(145, 'SpriteArgMorph', 'ArgMorph', 'morph');
	sb.addFields(147, 'BlockMorph', 'Morph', 'isSpecialForm,oldColor');
	sb.addFields(148, 'CommandBlockMorph', 'BlockMorph', 'commandSpec,argMorphs,titleMorph,receiver,selector,isReporter,isTimed,wantsName,wantsPossession');
	sb.addFields(149, 'CBlockMorph', 'CommandBlockMorph', 'nestedBlock,nextBlock');
	sb.addFields(151, 'HatBlockMorph', 'CommandBlockMorph', 'scriptNameMorph,indicatorMorph,scriptOwner,parameters,isClickable');
	sb.addFields(153, 'ScratchScriptsMorph', 'PasteUpMorph', '');
	sb.addFields(154, 'ScratchSliderMorph', 'AlignmentMorph', 'slider,sliderMin,sliderMax,variable');
	sb.addFields(155, 'WatcherMorph', 'AlignmentMorph', 'titleMorph,readout,readoutFrame,scratchSlider,watcher,isSpriteSpecific,unused,sliderMin,sliderMax,isLarge');
	sb.addFields(157, 'SetterBlockMorph', 'CommandBlockMorph', 'variable');
	sb.addFields(158, 'EventHatMorph', 'HatBlockMorph', '');
	sb.addFields(170, 'ReporterBlockMorph', 'CommandBlockMorph', 'isBoolean');
	sb.addFields(160, 'VariableBlockMorph', 'ReporterBlockMorph', '');
	sb.addFields(162, 'ImageMedia', 'ScratchMedia', 'form,rotationCenter,textBox,jpegBytes,compositeForm');
	sb.addFields(163, 'MovieMedia', 'ScratchMedia', 'fileName,fade,fadeColor,zoom,hPan,vPan,msecsPerFrame,currentFrame,moviePlaying');
	sb.addFields(164, 'SoundMedia', 'ScratchMedia', 'originalSound,volume,balance,compressedSampleRate,compressedBitsPerSample,compressedData');
	sb.addFields(165, 'KeyEventHatMorph', 'HatBlockMorph', '');
	sb.addFields(166, 'BooleanArgMorph', 'ArgMorph', '');
	sb.addFields(167, 'EventTitleMorph', 'ArgMorph', '');
	sb.addFields(168, 'MouseClickEventHatMorph', 'HatBlockMorph', '');
	sb.addFields(169, 'ExpressionArgMorphWithMenu', 'ExpressionArgMorph', 'menuMorph,getMenuSelector,specialValue');
	sb.addFields(171, 'MultilineStringMorph', 'BorderedMorph', 'fontSpec,textColor,selectionColor,lines');
	sb.addFields(172, 'ToggleButton', 'SimpleButtonMorph', 'onForm,offForm,overForm,disabledForm,isMomentary,toggleMode,isOn,isDisabled');
	sb.addFields(173, 'WatcherReadoutFrameMorph', 'BorderedMorph', '');
	sb.addFields(174, 'WatcherSliderMorph', 'SimpleSliderMorph', '');
	sb.addFields(175, 'ScratchListMorph', 'BorderedMorph', 'listName,strings,target,complex');
	sb.addFields(176, 'ScrollingStringMorph', 'BorderedMorph', 'fontSpec,showScrollbar,firstVisibleLine,textColor,selectionColor,lines');

	sb.addFields(180, 'ScrollFrameMorph2', 'ScrollFrameMorph', '');
	sb.addFields(181, 'ListMultilineStringMorph', 'MultilineStringMorph', '');
	sb.addFields(182, 'ScratchScrollBar', 'Morph', '');

	sb.addFields(200, 'CustomCommandBlockMorph', 'CommandBlockMorph', 'userSpec');
	sb.addFields(201, 'CustomBlockDefinition', '', 'userSpec,blockVars,isAtomic,isReporter,isBoolean,body,answer,type,category,declarations,defaults,isGlobal');
	sb.addFields(203, 'ReporterScriptBlockMorph', 'ReporterBlockMorph', '');
	sb.addFields(202, 'CommandScriptBlockMorph', 'ReporterScriptBlockMorph', '');
	sb.addFields(205, 'VariableFrame', '', 'vars');
	sb.addFields(206, 'CustomReporterBlockMorph', 'ReporterBlockMorph', 'userSpec');

	sb.addFields(207, 'CReporterSlotMorph', 'ReporterScriptBlockMorph', '');

	sb.addFields(300, 'StringFieldMorph', 'BorderedMorph', '');
	sb.addFields(301, 'MultiArgReporterBlockMorph', 'ReporterBlockMorph', '');

	(function (C, p) {

		p.on = function (bytes) {
			this.bytes = bytes;
			if (!bytes.subarray) bytes.subarray = bytes.slice;
			this.position = 0;
			return this;
		};
		p.readYPR = function (bytes) {
			var version, infoSize, info, stage;
			console.time('readSB');
			this.on(bytes);

			// skip header
			this.matchBytes([66, 108, 111, 120, 69, 120, 112, 86]);
			version = +(String.fromCharCode(this.next()) + String.fromCharCode(this.next()));
			if (version < 1) {
				throw new Error('Invalid version');
			}

			// read info
			infoSize = this.uint32();
			info = this.read();

			this.position = infoSize + 14; // header + uint32
			stage = this.read();

			this.onload({
				reader: this,
				info: info,
				stage: stage
			});
			console.timeEnd('readSB');
		};
		p.readInfo = function (bytes) {
			var version, infoSize, info;
			this.on(bytes);

			this.matchBytes([66, 108, 111, 120, 69, 120, 112, 86]);
			version = +(String.fromCharCode(this.next()) + String.fromCharCode(this.next()));
			if (version < 1) {
				throw new Error('Invalid version');
			}

			// read info
			infoSize = this.uint32();
			info = this.read();

			this.onload({
				reader: this,
				info: info
			});
		};
		p.read = function () {
			var i, objectCount;
			this.objects = [];
			this.readHeader();
			objectCount = this.uint32();

			i = objectCount;
			while (i--) {
				this.objects.push(this.readObject());
			}
			i = objectCount;
			while (i--) {
				this.fixReferences(this.objects[i]);
			}
			return this.objects[0][1];
		};
		p.fixReferences = function (object) {
			var classID = object[0],
				value = object[1],
				i = 0,
				fields, source;
			if (classID < 99) {
				this.fixFixedFormat(classID, value);
			} else {
				this.fixArray(object[3]);
				fields = sb.fields[classID];
				if (!fields)
					throw new Error('Invalid class ID ' + classID);
				source = value.fields;
				delete value.fields;
				value.className = sb.classNames[classID];
				i = fields.length;
				while (i--) {
					value[fields[i]] = source[i];
				}
			}
		};
		p.fixArray = function (a) {
			var i = a.length,
				o;
			while (i--) {
				if ((o = a[i]) && o.isRef) {
					if (o.id > this.objects.length) {
						throw new Error('Invalid object reference');
					}
					a[i] = this.objects[o.id - 1][1];
				}
			}
		};
		p.targetObjectFor = function (o) {
			if (o && o.isRef) {
				if (o.id > this.objects.length)
					throw new Error('Invalid object reference');
				return this.objects[o.id - 1][1];
			}
			return o;
		};
		p.fixFixedFormat = function (classID, object) {
			switch (classID) {
			case 20: // Array
			case 21: // OrderedCollection
			case 22: // Set
			case 23: // IdentitySet
				this.fixArray(object);
				return object;
			case 24: // Dictionary
			case 25: // IdentityDictionary
				this.fixArray(object.keys);
				this.fixArray(object.values);
				break;
			case 32: // Point
				object.x = this.targetObjectFor(object.x);
				object.y = this.targetObjectFor(object.y);
				break;
			case 33: // Rectangle
				object.origin.x = this.targetObjectFor(object.origin.x);
				object.origin.y = this.targetObjectFor(object.origin.y);
				object.corner.x = this.targetObjectFor(object.corner.x);
				object.corner.y = this.targetObjectFor(object.corner.y);
				break;
			case 34: // Form
				object.offset = this.targetObjectFor(object.offset);
				object.bits = this.targetObjectFor(object.bits);
				object.load();
				break;
			case 35: // ColorForm
				object.offset = this.targetObjectFor(object.offset);
				object.bits = this.targetObjectFor(object.bits);
				object.colors = this.targetObjectFor(object.colors);
				object.load();
				break;
			}
			return object;
		};
		p.readObject = function () {
			var classID = this.next(),
				version, fieldCount, fields;
			if (classID > 99) {
				version = this.next();
				fieldCount = this.next();
				fields = [];
				while (fieldCount--) {
					fields.push(this.readField());
				}
				return [classID, { fields: fields }, version, fields];
			}
			return [classID, this.readFixedFormat(classID)];
		};
		p.readField = function () {
			var classID = this.next();
			if (classID === 99) {
				return new sb.Ref(this.uint24());
			}
			return this.readFixedFormat(classID);
		};
		p.readFixedFormat = function (classID) {
			var a, n;
			switch (classID) {
			case 1: // UndefinedObject
				return null;
			case 2: // True
				return true;
			case 3: // False
				return false;
			case 4: // SmallInteger
				return this.int32();
			case 5: // SmallInteger16
				return this.int16();
			case 6: // LargePositiveInteger
			case 7: // LargeNegativeInteger
				n = this.uint16();
				a = 0;
				while (n--) {
					a *= 0x100;
					a += this.next();
				}
				return a;
			case 8: // Float
				return this.float64();
			case 9: // String
			case 10: // Symbol
			case 14: // UTF8
				a = [].slice.call(this.readBytes(n = this.uint32()));
				while (n--) {
					a[n] = String.fromCharCode(a[n]);
				}
				return a.join('');
			case 11: // ByteArray
				return this.readBytes(this.uint32());
			case 12: // SoundBuffer
				a = [];
				n = this.uint32();
				while (n--) {
					a.push(this.int16());
				}
				return a;
			case 13: // Bitmap
				a = [];
				a.isBitmap = true;
				n = this.uint32();
				while (n--) {
					a.push(this.uint32());
				}
				return a;
			case 20: // Array
			case 21: // OrderedCollection
			case 22: // Set
			case 23: // IdentitySet
				a = [];
				n = this.uint32();
				while (n--) {
					a.push(this.readField());
				}
				return a;
			case 24: // Dictionary
			case 25: // IdentityDictionary
				a = new sb.Dictionary([], []);
				n = this.uint32();
				while (n--) {
					a.keys.push(this.readField());
					a.values.push(this.readField());
				}
				return a;
			case 30: // Color
				return new sb.Color(this.uint32(), 255);
			case 31: // TranslucentColor
				return new sb.Color(this.uint32(), this.uint8());
			case 32: // Point
				return new sb.Point(this.readField(), this.readField());
			case 33: // Rectangle
				return new sb.Rectangle(this.readField(), this.readField(), this.readField(), this.readField());
			case 34: // Form
				return new sb.Form(this.readField(), this.readField(), this.readField(), this.readField(), this.readField());
			case 35: // ColorForm
				return new sb.ColorForm(this.readField(), this.readField(), this.readField(), this.readField(), this.readField(), this.readField());
			}
			throw new Error('Invalid fixed-format class ID');
		};
		p.readHeader = function () {
			this.matchBytes([79, 98, 106, 83, 1, 83, 116, 99, 104, 1]);
		};
		p.readBytes = function (length) {
			return this.bytes.subarray(this.position, this.position += length);
		};
		p.matchBytes = function (bytes) {
			var i = bytes.length,
				r = this.readBytes(i);
			while (i--) {
				if (r[i] !== bytes[i]) {
					throw new Error('Invalid format');
				}
			}
		};
		p.skip = function (length) {
			this.position += length;
		};
		p.next = p.uint8 = function () {
			return this.bytes[this.position++];
		};
		p.hasNext = function () {
			return this.position < this.bytes.length;
		};
		p.uint16 = function () {
			return this.next() * 0x100 + this.next();
		};
		p.uint24 = function () {
			return this.next() * 0x10000 + this.next() * 0x100 + this.next();
		};
		p.uint32 = function () {
			return this.next() * 0x1000000 + this.next() * 0x10000 + this.next() * 0x100 + this.next();
		};
		p.int8 = function () {
			var v = this.bytes[++this.position];
			return v >= 0x80 ? v - 0x100 : v;
		};
		p.int16 = function () {
			var d = this.next(),
				v = d * 0x100 + this.next();
			return d >= 0x80 ? v - 0x10000 : v;
		};
		p.int24 = function () {
			var d = this.next(),
				v = d * 0x10000 + this.next() * 0x100 + this.next();
			return d >= 0x80 ? v - 0x1000000 : v;
		};
		p.int32 = function () {
			var d = this.next(),
				v = d * 0x1000000 + this.next() * 0x10000 + this.next() * 0x100 + this.next();
			return d >= 0x80 ? v - 0x100000000 : v;
		};
		p.string = function () {
			var length = this.uint16(),
				bytes = this.readBytes(length),
				i = length;
			while (i--) {
				bytes[i] = String.fromCharCode(bytes[i]);
			}
			return bytes.join('');
		};
		p.float64 = function () {
			return this.ieee(8, 11, 52, 1023);
		};
		p.ieee = function (n, ebits, mbits) {
			var bias = (1 << (ebits - 1)) - 1,
				string = '',
				i = n,
				b, sign, exponent, mantissa, result;
			while (i--) {
				b = this.next().toString(2);
				string = string + Array(9 - b.length).join('0') + b;
			}
			sign = string.charAt(0) === '0' ? 1 : -1;
			exponent = parseInt(string.substr(1, ebits), 2);
			mantissa = parseInt(string.substr(ebits + 1), 2);
			if (exponent === 0) {
				return mantissa === 0 ? sign * 0 : sign * Math.pow(2, 1 - bias) * mantissa / Math.pow(2, mbits);
			}
			if (exponent === (1 << ebits) - 1) {
				return mantissa === 0 ? sign / 0 : NaN;
			}
			return sign * Math.pow(2, exponent - bias) * (1 + mantissa / Math.pow(2, mbits));
		};

	})(sb.Reader = function () {}, sb.Reader.prototype);

	(function (C, p) {

		var rotationStyles = {
			normal: 1,
			leftRight: 2,
			none: 0
		}, customBlockInputs = {
			object: '%obj',
			objectList: '%mult%obj',
			number: '%n',
			numberList: '%mult%n',
			text: '%txt',
			textList: '%mult%txt',
			list: '%l',
			listList: '%mult%l',
			any: '%s',
			anyList: '%mult%s',
			'boolean': '%b',
			booleanList: '%mult%b',
			command: '%cmdRing',
			commandList: '%mult%cmdRing',
			reporter: '%repRing',
			reporterList: '%mult%repRing',
			predicate: '%predRing',
			predicateList: '%mult%predRing',
			loop: '%cs',
			loopList: '%mult%cs',
			unevaluated: '%anyUE',
			unevaluatedList: '%mult%anyUE',
			unevaluatedBoolean: '%boolUE',
			unevaluatedBooleanList: '%mult%boolUE',
			template: '%upvar'
		}, blockSelectors = {
			// Motion': '',
			'forward:': 'forward',
			'turnLeft:': 'turnLeft',
			'turnRight:': 'turn',
			'heading:': 'setHeading',
			'pointTowards:': 'doFaceTowards',
			'gotoX:y:': 'gotoXY',
			'gotoSpriteOrMouse:': 'doGotoObject',
			'glideSecs:toX:y:elapsed:from:': 'doGlide',
			'changeXposBy:': 'changeXPosition',
			'xpos:': 'setXPosition',
			'changeYposBy:': 'changeYPosition',
			'ypos:': 'setYPosition',
			'bounceOffEdge': 'bounceOffEdge',
			'xpos': 'xPosition',
			'ypos': 'yPosition',
			'heading': 'direction',

			// Looks
			'lookLike:': 'doSwitchToCostume',
            'showBackground:': 'doSwitchToCostume',
			'nextCostume': 'doWearNextCostume',
			'nextBackground': 'doWearNextCostume',
			'costumeIndex': 'getCostumeIdx',
			'say:duration:elapsed:from:': 'doSayFor',
			'say:': 'bubble',
			'think:duration:elapsed:from:': 'doThinkFor',
			'think:': 'doThink',
			'changeGraphicEffect:by:': 'changeEffect',
			'setGraphicEffect:to:': 'setEffect',
			'filterReset': 'clearEffects',
			'setSizeTo:': 'setScale',
            'changeSizeBy:': 'changeScale',
			'scale': 'getScale',
			'show': 'show',
			'hide': 'hide',
			'comeToFront': 'comeToFront',
			'goBackByLayers:': 'goBack',

			// Sound
			'playSound:': 'playSound',
			'doPlaySoundAndWait': 'doPlaySoundUntilDone',
			'stopAllSounds': 'doStopAllSounds',
			// 'drum:duration:elapsed:from:': '',
			'rest:elapsed:from:': 'doRest',
			'noteOn:duration:elapsed:from:': 'doPlayNote',
			// 'midiInstrument:': '',
			// 'changeVolumeBy:': '',
			// 'setVolumeTo:': '',
			// 'volume': '',
			'changeTempoBy:': 'doChangeTempo',
			'setTempoTo:': 'doSetTempo',
			'tempo': 'getTempo',

			// Pen
			'clearPenTrails': 'clear',
			'putPenDown': 'down',
			'putPenUp': 'up',
			'penColor:': 'setColor',
			'changePenHueBy:': 'changeHue',
			'setPenHueTo:': 'setHue',
            '_changePenHueBy:': 'changeHue',
            '_setPenHueTo:': 'setHue',
			'changePenShadeBy:': 'changeBrightness',
			'setPenShadeTo:': 'setBrightness',
			'changePenSizeBy:': 'changeSize',
			'penSize:': 'setSize',
			'stampCostume': 'doStamp',

			// Control
			// 'whenStartClicked': '',
			// 'whenKeyPressed:': '',
			// 'whenSpriteClicked': '',
			'wait:elapsed:from:': 'doWait',
			'doForever': 'doForever',
			'doRepeat': 'doRepeat',
			'broadcast:': 'doBroadcast',
			'doBroadcastAndWait': 'doBroadcastAndWait',
			// 'whenMessageReceived:': '',
			// TODO 'doForeverIf': '',
			'doIf': 'doIf',
			'doIfElse': 'doIfElse',
			'doWaitUntil': 'doWaitUntil',
			'doUntil': 'doUntil',
			'doReturn': 'doStop',
			'stopAll': 'doStopAll',
			'doRun': 'doRun',
			'doRunBlockWithArgs': 'doRun',
			'doRunBlockWithArgList': 'doRun',
			'doFork': 'fork',
			'doForkBlockWithArgs': 'fork',
			'doForkBlockWithArgList': 'fork',
			'doReport': 'evaluate',
			'doCallBlockWithArgs': 'evaluate',
			'doCallBlockWithArgList': 'evaluate',
			'doAnswer': 'doReport',
			'doStopBlock': 'doStopBlock',
			// 'doPauseThread': '',
			// 'doPauseThreadReporter': '',

			// Sensing
			'touching:': 'reportTouchingObject',
			'touchingColor:': 'reportTouchingColor',
			'color:sees:': 'reportColorIsTouchingColor',
			'doAsk': 'doAsk',
			'answer': 'reportLastAnswer',
			'mouseX': 'reportMouseX',
			'mouseY': 'reportMouseY',
			'mousePressed': 'reportMouseDown',
			'keyPressed:': 'reportKeyPressed',
			'distanceTo:': 'reportDistanceTo',
			'timerReset': 'doResetTimer',
			'timer': 'reportTimer',
			'getAttribute:of:': 'reportAttributeOf',
			// 'attribute:of:': '',
			// 'soundLevel': '',
			// 'isLoud': '',
			// 'sensor:': '',
			// 'sensorPressed:': '',
			// 'getObject:': '',
			// 'get:': '',

			// Operators
			'+': 'reportSum',
			'-': 'reportDifference',
			'*': 'reportProduct',
			'/': 'reportQuotient',
			'randomFrom:to:': 'reportRandom',
			'<': 'reportLessThan',
			'=': 'reportEquals',
			'>': 'reportGreaterThan',
			'&': 'reportAnd',
			'|': 'reportOr',
			'not': 'reportNot',
			'getTrue': 'reportTrue',
			'getFalse': 'reportFalse',
			'concatenate:with:': 'reportJoinWords',
			'letter:of:': 'reportLetter',
			'stringLength:': 'reportStringSize',
			'asciiCodeOf:': 'reportUnicode',
			'asciiLetter:': 'reportUnicodeAsLetter',
			'\\\\': 'reportModulus',
			'rounded': 'reportRound',
			'computeFunction:of:': 'reportMonadic',
			'isObject:type:': 'reportIsA',
			// 'procedure': 'reifyScript',
			// 'procedureWithArgs': 'reifyScript',
			// 'function': 'reifyReporter',
			// 'functionWithArgs': 'reifyReporter',
			// 'spawn': '',

			// Variables
			'setVar:to:': 'doSetVar',
			'changeVar:by:': 'doChangeVar',
			// 'deleteObject:': '',
			'showVariable:': 'doShowVar',
			'hideVariable:': 'doHideVar',
			'doDeclareVariables': 'doDeclareVariables',
			'newList:': 'reportNewList',
			'append:toList:': 'doAddToList',
			'deleteLine:ofList:': 'doDeleteFromList',
			'insert:at:ofList:': 'doInsertInList',
			'setLine:ofList:to:': 'doReplaceInList',
			'getLine:ofList:': 'reportListItem',
			'lineCountOfList:': 'reportListLength',
			'list:contains:': 'reportListContainsItem',
			// 'contentsOfList:': '',
			// 'copyOfList:': ''

			// Kludge
			_warp: 'doWarp'
		};

		function escapeXML(string) {
            // over-cautious due to some morphic bugs
			return string.replace(/&/g, '&amp;')/*.replace(/[\n\r]|\r\n/g, '&#10;')*/.replace(/</g, '&lt;').replace(/"/g, '&quot;');
		}
		function costumeIndex(object) {
			return object.media.filter(function (m) { return m.className === 'ImageMedia' }).indexOf(object.costume) + 1;
		}

		function Text(value) {
			this.value = '' + value;
		}
		Text.prototype.toXML = function (string) {
			string.push(escapeXML(this.value));
		};
        Text.prototype.toDOM = function () {
            return document.createTextNode(this.value);
        };

		function Element(tagName) {
			this.tagName = tagName;
		}
		Element.prototype.toXML = function (string) {
			var attributes = this.attributes, key;
			string.push('<', this.tagName);
			for (key in attributes) if (attributes.hasOwnProperty(key)) {
				string.push(' ', key, '="', escapeXML('' + attributes[key]), '"');
			}
			if (this.children.length) {
				string.push('>');
				this.children.forEach(function (child) {
					if (!child.toXML) {
						console.error('Invalid', child);
					}
					child.toXML(string);
				});
				string.push('</', this.tagName, '>');
			} else {
				string.push('/>');
			}
		};
        Element.prototype.toDOM = function () {
            var el = document.createElement(this.tagName),
                attributes = this.attributes, key,
                children = this.children, i = 0, child;
            for (key in attributes) if (attributes.hasOwnProperty(key)) {
                el.setAttribute(key, attributes[key]);
            }
            while (child = children[i++]) {
                el.appendChild(child.toDOM());
            }
            return el;
        };

		function n(tagName, attributes, children) {
			var el = new Element(tagName), key;
			el.attributes = attributes || {};
			el.children = children || [];
			return el;
		}
		function t(value) {
			return new Text(value);
		}

		p.write = function (projectName, project) {
			var a, xml, out, objectID = 0, customBlocks = {};
			function id(n) {
				return n.attributes.id ? n('ref', { id: n.attributes.id }) : n.attributes.id = ++objectID, n;
			}
			function preCustomBlocks(scope, blocks) {
				var sc = customBlocks[scope] = {};
				if (!blocks) return;
				blocks.forEach(function (ypr) {
					sc[ypr.userSpec] = ypr;
					ypr.userSpec = ypr.userSpec.replace(/^\s+|\s+$/g, '').replace(/\s{2,}/g, ' ').replace(/'/g, '');
					ypr.__blockSpec__ = ypr.userSpec.replace(/%(\S+)/g, function (_, name) {
						var type = customBlockInputs[ypr.declarations.get(name)];
						if (type === undefined) {
							if (ypr.declarations.get(name) === undefined) {
								type = '%s';
							} else {
								console.warn('Missing input type ' + ypr.declarations.get(name));
							}
						}
						return type;
					});
					ypr.__types__ = (ypr.userSpec.match(/%\S+/g) || []).map(function (arg) {
						return ypr.declarations.get(arg.substr(1)) || 'any';
					});
				});
			}
			function nsCustomBlocks(blocks, globals) {
				if (!blocks) return [];
				return (globals ? blocks : blocks.filter(function (b) { return !b.isGlobal })).map(function (ypr) {
					var args = (ypr.userSpec.match(/%\S+/g) || []).map(function (arg) {
							var name = arg.substr(1),
								type = customBlockInputs[ypr.declarations.get(name)];
							if (type === undefined) {
								type = '%s';
							}
							return n('input', {
								type: type
							}, [t(ypr.defaults.get(name))]);
						}),
						spec = ypr.userSpec.replace(/%(\S+)/g, '%\'$1\'');
					ypr.body = ypr.body || [];
					if (ypr.answer && ypr.answer.pop) {
						ypr.body.push(['byob', '', 'doAnswer', ypr.answer[0]]);
					}
                    if (ypr.blockVars.length) {
                        ypr.body.unshift(['byob', '', 'doDeclareVariables'].concat(ypr.blockVars.map(function (n) {
                            return [0, 0, 0, n];
                        })));
                    }
					if (ypr.isAtomic) {
						ypr.body = [['byob', '', '_warp', ypr.body]];
					}
					return n('block-definition', {
						s: spec,
						// type: ypr.isBoolean ? 'predicate' : ypr.isReporter ? 'reporter' : 'command',
						type: ypr.type === 'boolean' ? 'predicate' : ypr.answer !== null ? 'reporter' : 'command',
						category:
							ypr.category === 'none' ? 'other' :
							ypr.category === 'list' ? 'lists' : ypr.category
					}, [
						n('inputs', {}, args),
						n('script', {}, nsScript(ypr.body))
					]);
				});
			}
			function nsScript(script) {
				return script.map(function (block) {
					return nBlock(block);
				});
			}
			function nBlock(block) {
				var node, a;
				node = n('block');
				switch (block[2]) {
				case 'EventHatMorph':
					if (block[3] === 'Scratch-StartClicked') {
						node.attributes.s = 'receiveGo';
					} else {
						node.attributes.s = 'receiveMessage';
						node.children.push(n('l', {}, [t(block[3])]));
					}
					return node;
				case 'MouseClickEventHatMorph':
					node.attributes.s = 'receiveClick';
					return node;
				case 'KeyEventHatMorph':
					node.attributes.s = 'receiveKey';
					node.children.push(n('l', {}, [t(block[3])]));
					return node;
				case 'doRunBlockWithArgs':
				case 'doCallBlockWithArgs':
				case 'doForkBlockWithArgs':
					a = block.length - 5;
					while (a--) {
						if (block[5 + a].pop) {
							block[5 + a] = ['block', '', [block[5 + a]]];
						}
					}
					a = {
						className: 'ScratchListMorph',
						complex: block.slice(5)
					};
					block = block.slice(0, 4);
					block.push(a);
					break;
				case 'doRunBlockWithArgList':
				case 'doCallBlockWithArgList':
				case 'doForkBlockWithArgList':
					a = block[5];
					block = block.slice(0, 4);
					block.push(a);
					break;
				case 'doDeclareVariables':
					block = ['byob', '', 'doDeclareVariables', {
						className: 'ScratchListMorph',
						complex: block.slice(3).map(function (block) {
							return block[3];
						})
					}];
					break;
				case 'getLine:ofList:':
				case 'append:toList:':
				case 'deleteLine:ofList:':
				case 'setLine:ofList:to:':
				case 'getLine:ofList:':
				case 'insert:at:ofList:':
				case 'lineCountOfList:':
				case 'list:contains:':
					a = block[2] === 'insert:at:ofList:' ? 5 :
						block[2] === 'lineCountOfList:' || block[2] === 'list:contains:' ? 3 : 4;
					if (typeof block[a] === 'string') {
						block[a] = block[a] === '' ? null : ['byob', '', 'readVariable', block[a]];
					}
					break;
				case 'readBlockVariable':
				case 'readVariable':
				case 'listNamed:':
					node.attributes['var'] = block[3];
					return node;
				case 'function':
				case 'functionWithArgs':
					node.attributes.s = 'reifyReporter';
					block.pop();
					node.children.push(n('autolambda', {}, [nBlock(block[8])]));
					if (block[2] === 'functionWithArgs') {
						node.children.push(n('list', {}, block.slice(9).map(function (arg) {
							return n('l', {}, [t(arg[3])]);
						})));
					}
					return node;
				case 'procedure':
				case 'procedureWithArgs':
					node.attributes.s = 'reifyScript';
					node.children.push(n('script', {}, nsScript(block.pop())));
					node.children.push(n('list', {}, block.slice(8).map(function (arg) {
						return n('l', {}, [t(arg[3])]);
					})));
					return node;
				case 'autoBlock':
				case 'autoPredicate':
					node.attributes.s = block[2] === 'autoPredicate' ? 'reifyPredicate' : 'reifyReporter';
					node.children.push(n('autolambda', {}, block[3] ? [nBlock(block[3][0])] : []));
					return node;
				case 'concatenate:with:':
					block = block.slice(0, 3).concat({
						className: 'ScratchListMorph',
						complex: block.slice(3)
					});
					a = 2;
					while (a--) {
						if (block[3].complex[a].pop) {
							block[3].complex[a] = ['block', '', [block[3].complex[a]]];
						}
					}
					break;
				case 'loopLambda':
					return n('script', {}, block[8] ? nsScript(block[8]) : []);
				case 'autoLambda':
					// node.attributes.s = block[2] === 'loopLambda' ? 'reifyScript' : 'reifyReporter';
                    node.attributes.s = 'reifyScript';
					node.children.push(n('script', {}, block[8] ? nsScript(block[8]) : []));
					return node;
				case 'changeVariable':
				case 'changeBlockVariable':
					block = [0, 0, block[4], block[3], block[block[2] === 'changeBlockVariable' ? 6 : 5]];
					break;
                case 'distanceTo:':
                case 'gotoSpriteOrMouse:':
                case 'pointTowards:':
                case 'touching:':
                    if (block[3] === 'mouse') {
                        block[3] = 'mouse-pointer';
                    } else {
                        block[3] = block[3].objName;
                    }
                    break;
                case 'doForeverIf':
                    block = [0, 0, 'doForever', [[0, 0, 'doIf', block[3], block[4]]]]
                    break;
                case '\\\\':
                case '+':
                case '-':
                case '*':
                case '/':
                    if (block[3] === ' ') {
                        block[3] = '';
                    }
                    if (block[4] === ' ') {
                        block[4] = '';
                    }
                    break;
                case 'setPenHueTo:':
                case 'changePenHueBy:':
                    block[3] = ['byob', 0, '/', block[3], '2'];
                    break;
				}
				if (block[2] === 'doCustomBlock') {
					a = customBlocks[block[1] === 'Stage' ? '__global__' : block[1]][block[3]];
					node = n('custom-block', {
						s: a.__blockSpec__
					});
					if (block[1] !== 'Stage' && !a.isGlobal) {
						node.attributes.scope = block[1];
					}
					a.__types__.forEach(function (type, i) {
						if (type === 'template') {
							block[4 + i] = block[4 + i][3];
						}
					});
					block.shift();
				} else {
					if ((node.attributes.s = blockSelectors[block[2]]) === undefined) {
						console.warn('Missing selector mapping for "' + block[2] + '"', block);
					}
				}
				block.slice(3).forEach(function (arg) {
					if (arg && arg.pop) {
						if (typeof arg[0] === 'string') {
							node.children.push(nBlock(arg));
						} else {
							node.children.push(n('script', {}, nsScript(arg)));
						}
					} else {
                        a = nValue(arg);
                        delete a.attributes.id;
						node.children.push(a);
					}
				});
				return node;
			}
			function nsVariables(vars, lists) {
				return vars.keys.map(function (key, i) {
					return n('variable', { name: key }, [nValue(vars.values[i], true)]);
				}).concat(lists.keys.map(function (key, i) {
					return n('variable', { name: key }, [nValue(lists.values[i], true)]);
				}));
			}
			function nValue(value, listHasItems) {
				return typeof value === 'string' || typeof value === 'number' ? n('l', {}, [t(value)]) :
					value == null || typeof value === 'boolean' ? n('l') :
					value.className === 'ScratchListMorph' ? id(n('list', {}, value.complex.map(function (object, i) {
						var item = object == null ? n('l', {}, [t((item = value.strings[i]) === 'nil' ? '' : item)]) :
							typeof object === 'string' ? nValue(object) :
							object && object[0] === 'block' ? nsScript(object[2])[0] : n('l');
						return listHasItems ? n('item', {}, [item]) : item;
					}))) :
					// (function () { throw 'Unimplemented' })();
					value.constructor === sb.Color ? n('color', {}, [t([value.r * 255, value.g * 255, value.b * 255, value.a * 255])]) :
					value.constructor === sb.Dictionary ? id(n('list')) :
					(function () { console.warn('No serializer for', value); return n('__undefined__') })();
			}
			function nsCostumes(media) {
				return [id(n('list', {}, media.filter(function (m) { return m.className === 'ImageMedia' }).map(function (c) {
					return n('item', {}, [
						n('costume', {
							name: c.mediaName,
							'center-x': c.rotationCenter.x,
							'center-y': c.rotationCenter.y,
							image: c.form.image.toDataURL()
						})
					]);
				})))];
			}
			function nsSounds(media) {
				if (media.filter(function (m) { return m.className === 'SoundMedia' }).length > 0) {
					console.warn('Sounds are unimplemented');
				}
				return [id(n('list'))];
			}
			function nsScripts(scripts) {
				return scripts.map(function (script) {
					if (script[1][0] && script[1][0][2] === 'scratchComment') {
						return n('comment', {
							x: script[0].x,
							y: script[0].y,
							w: script[1][0][5],
							collapsed: !script[1][0][4]
						}, [t(script[1][0][3])]);
					}
					return n('script', {
						x: script[0].x,
						y: script[0].y
					}, nsScript(script[1]));
				});
			}
			function nsSprites(sprites, vars, lists) {
				return sprites.map(function (sprite, i) {
					return id(n('sprite', {
						name: sprite.objName,
						x: (sprite.bounds.origin.x + sprite.bounds.corner.x) / 2 - 240,
						y: 180 - (sprite.bounds.origin.y + sprite.bounds.corner.y) / 2,
                        hidden: sprite.flags === 1,
						heading: sprite.rotationDegrees + 90,
						scale: sprite.scalePoint.x,
						rotation: rotationStyles[sprite.rotationStyle],
						draggable: sprite.draggable,
						costume: costumeIndex(sprite),
						color: (sprite.color.r * 255 | 0) + ',' + (sprite.color.g * 255 | 0) + ',' + (sprite.color.b * 255 | 0),
                        pen: 'tip',
                        idx: i
					}, [
						n('variables', {}, nsVariables(sprite.vars, sprite.lists)),
						n('costumes', {}, nsCostumes(sprite.media)),
						n('sounds', {}, nsSounds(sprite.media)),
						n('blocks', {}, nsCustomBlocks(sprite.customBlocks, false)),
						n('scripts', {}, nsScripts(sprite.blocksBin))
					]));
				}).concat(vars.keys.concat(lists.keys).map(function (name) {
					return n('watcher', {
						'var': name,
						style: 'normal',
						x: 10,
						y: 10,
						color: '243,118,29',
						hidden: true
					});
				}));
			}

			console.log(project);
			preCustomBlocks('__global__', project.stage.customBlocks);
			project.stage.sprites.forEach(function (sprite) {
				preCustomBlocks(sprite.objName, sprite.customBlocks);
			});
			xml = n('project', {
				name: projectName,
				app: 'Snap! 4.0, http://snap.berkeley.edu; serialized by yprxml, http://dl.dropbox.com/u/10715865/Web/snap/app/yprxml.html',
				version: '1'
			}, [
				n('notes', {}, [t(project.info.get('comment') || '')]),
				n('thumbnail', {}, [t((a = project.info.get('thumbnail')) ? a.image.toDataURL() : '')]),
				id(n('stage', {
                    name: 'Stage',
                    tempo: project.stage.tempoBPM,
					costume: costumeIndex(project.stage),
					threadsafe: 'false',
                    scheduled: true
				}, [
					n('pentrails', {}, [t((a = project.info.get('penTrails')) ? a.image.toDataURL() : '')]),
					n('costumes', {}, nsCostumes(project.stage.media)),
					n('sounds', {}, nsSounds(project.stage.media)),
					n('blocks', {}, []),
					n('variables', {}, []),
					n('scripts', {}, nsScripts(project.stage.blocksBin)),
					n('sprites', {}, nsSprites(project.stage.sprites, project.stage.vars, project.stage.lists))
				])),
				n('variables', {}, nsVariables(project.stage.vars, project.stage.lists)),
				n('blocks', {}, nsCustomBlocks(project.stage.customBlocks, true))
			]);
			xml.toXML(out = []);
            // var d = document.createElement('textarea');
            // d.style.position = 'absolute';
            // document.body.appendChild(d).value = out.join('');
			return out.join('');
		};

	})(sb.XMLWriter = function () {}, sb.XMLWriter.prototype);

	return sb;

})({});
