/*
    store.js
    saving and loading Snap! projects
    written by Jens Mönig
    jens@moenig.org
    Copyright (C) 2019 by Jens Mönig
    This file is part of Snap!.
    Snap! is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of
    the License, or (at your option) any later version.
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.
    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see .
    prerequisites:
    --------------
    needs morphic.js, xml.js, and most of Snap!'s other modules
    hierarchy
    ---------
    the following tree lists all constructors hierarchically,
    indentation indicating inheritance. Refer to this list to get a
    contextual overview:
        XML_Serializer
            SnapSerializer
    credits
    -------
    Nathan Dinsmore contributed to the design and implemented a first
    working version of a complete XMLSerializer. I have taken much of the
    overall design and many of the functions and methods in this file from
    Nathan's fine original prototype.
*/
/*global modules, XML_Element, VariableFrame, StageMorph, SpriteMorph,
WatcherMorph, Point, CustomBlockDefinition, Context, ReporterBlockMorph,
CommandBlockMorph, detect, CustomCommandBlockMorph, CustomReporterBlockMorph,
Color, List, newCanvas, Costume, Sound, Audio, IDE_Morph, ScriptsMorph,
BlockMorph, ArgMorph, InputSlotMorph, TemplateSlotMorph, CommandSlotMorph,
FunctionSlotMorph, MultiArgMorph, ColorSlotMorph, nop, CommentMorph, isNil,
localize, sizeOf, ArgLabelMorph, SVG_Costume, MorphicPreferences, Process,
SyntaxElementMorph, Variable, isSnapObject, console, BooleanSlotMorph,
normalizeCanvas, contains*/
// Global stuff ////////////////////////////////////////////////////////
modules.store = '2019-August-08';
// XML_Serializer ///////////////////////////////////////////////////////
/*
    I am an abstract protype for my heirs.
    I manage object identities and keep track of circular data structures.
    Objects are "touched" and a property named "serializationID" is added
    to each, representing an index integer in the list, starting with 1.
*/
// XML_Serializer instance creation:
function XML_Serializer() {
    this.contents = [];
    this.media = [];
    this.isCollectingMedia = false;
    this.isExportingBlocksLibrary = false;
}
// XML_Serializer preferences settings:
XML_Serializer.prototype.idProperty = 'serializationID';
XML_Serializer.prototype.mediaIdProperty = 'serializationMediaID';
XML_Serializer.prototype.mediaDetectionProperty = 'isMedia';
XML_Serializer.prototype.version = 1; // increment on structural change
// XML_Serializer accessing:
XML_Serializer.prototype.serialize = function (object, forBlocksLibrary) {
    // public: answer an XML string representing the given object
    var xml;
    this.flush(); // in case an error occurred in an earlier attempt
    this.flushMedia();
    this.isExportingBlocksLibrary = forBlocksLibrary;
    xml = this.store(object);
    this.flush();
    return xml;
};
XML_Serializer.prototype.store = function (object, mediaID) {
    // private - mediaID is optional
    if (isNil(object) || !object.toXML) {
        // unsupported type, to be checked before calling store()
        // when debugging, be sure to throw an error at this point
        return '';
    }
    if (this.isCollectingMedia && object[this.mediaDetectionProperty]) {
        this.addMedia(object, mediaID);
        return this.format(
            '%' +
            '%' +
            '%' +
            '',
        (ide && ide.projectName) ? ide.projectName : localize('Untitled'),
        serializer.app,
        serializer.version,
        (ide && ide.projectNotes) ? ide.projectNotes : '',
        thumbdata,
        this.name,
        StageMorph.prototype.dimensions.x,
        StageMorph.prototype.dimensions.y,
        costumeIdx,
        this.color.r,
        this.color.g,
        this.color.b,
        this.color.a,
        this.getTempo(),
        this.isThreadSafe,
        this.instrument ?
                ' instrument="' + parseInt(this.instrument) + '" ' : '',
        this.volume,
        this.pan,
        SpriteMorph.prototype.useFlatLineEnds ? 'flat' : 'round',
        BooleanSlotMorph.prototype.isTernary,
        this.enableCodeMapping,
        this.enableInheritance,
        this.enableSublistIDs,
        StageMorph.prototype.frameRate !== 0,
        normalizeCanvas(this.trailsCanvas, true).toDataURL('image/png'),
        // current costume, if it's not in the wardrobe
        !costumeIdx && this.costume ?
            '' + serializer.store(this.costume) + ''
                : '',
        serializer.store(this.costumes, this.name + '_cst'),
        serializer.store(this.sounds, this.name + '_snd'),
        serializer.store(this.variables),
        serializer.store(this.customBlocks),
        serializer.store(this.scripts),
        serializer.store(this.children),
        Object.keys(StageMorph.prototype.hiddenPrimitives).reduce(
                function (a, b) {return a + ' ' + b; },
                ''
            ),
        code('codeHeaders'),
        code('codeMappings'),
        serializer.store(this.globalBlocks),
        (ide && ide.globalVariables) ?
                    serializer.store(ide.globalVariables) : ''
    );
};
SpriteMorph.prototype.toXML = function (serializer) {
    var stage = this.parentThatIsA(StageMorph),
        ide = stage ? stage.parentThatIsA(IDE_Morph) : null,
        idx = ide ? ide.sprites.asArray().indexOf(this) + 1 : 0,
        costumeIdx = this.getCostumeIdx(),
        noCostumes = this.inheritsAttribute('costumes'),
        noSounds = this.inheritsAttribute('sounds'),
        noScripts = this.inheritsAttribute('scripts');
    return serializer.format(
        '' +
            '%' + // inheritance info
            '%' + // nesting info
            '%' + // current costume
            (noCostumes ? '%' : '%') +
            (noSounds ? '%' : '%') +
            '%' +
            '%' +
            (this.exemplar ? '%' : '%') +
            (noScripts ? '%' : '%') +
            '',
        this.name,
        idx,
        this.xPosition(),
        this.yPosition(),
        this.heading,
        this.scale,
        this.volume,
        this.pan,
        this.rotationStyle,
        this.instrument ?
                ' instrument="' + parseInt(this.instrument) + '" ' : '',
        this.isDraggable,
        this.isVisible ? '' : ' hidden="true"',
        costumeIdx,
        this.color.r,
        this.color.g,
        this.color.b,
        this.color.a,
        this.penPoint,
        // inheritance info
        this.exemplar
            ? '' +
                    (this.inheritedAttributes.length ?
                        serializer.store(new List(this.inheritedAttributes))
                        : '') +
                    ''
            : '',
        // nesting info
        this.anchor
            ? ''
            : '',
        // current costume, if it's not in the wardrobe
        !costumeIdx && this.costume ?
            '' + serializer.store(this.costume) + ''
                : '',
        noCostumes ? '' : serializer.store(this.costumes, this.name + '_cst'),
        noSounds ? '' : serializer.store(this.sounds, this.name + '_snd'),
        !this.customBlocks ? '' : serializer.store(this.customBlocks),
        serializer.store(this.variables),
        this.exemplar ? serializer.store(this.inheritedMethods()) : '',
        noScripts ? '' : serializer.store(this.scripts)
    );
};
Costume.prototype[XML_Serializer.prototype.mediaDetectionProperty] = true;
Costume.prototype.toXML = function (serializer) {
    return serializer.format(
        '',
        this.name,
        this.rotationCenter.x,
        this.rotationCenter.y,
        this instanceof SVG_Costume ? this.contents.src
                : normalizeCanvas(this.contents).toDataURL('image/png')
    );
};
Sound.prototype[XML_Serializer.prototype.mediaDetectionProperty] = true;
Sound.prototype.toXML = function (serializer) {
    return serializer.format(
        '',
        this.name,
        this.toDataURL()
    );
};
VariableFrame.prototype.toXML = function (serializer) {
    var myself = this;
    return Object.keys(this.vars).reduce(function (vars, v) {
        var val = myself.vars[v].value,
            dta;
        if (myself.vars[v].isTransient) {
            dta = serializer.format(
                '',
                v)
            ;
        } else if (val === undefined || val === null) {
            dta = serializer.format('', v);
        } else {
            dta = serializer.format(
                '%',
                v,
                typeof val === 'object' ?
                        (isSnapObject(val) ? ''
                                : serializer.store(val))
                                : typeof val === 'boolean' ?
                                        serializer.format(
                                            '$', val
                                        )
                                        : serializer.format('$', val)
            );
        }
        return vars + dta;
    }, '');
};
// Watchers
WatcherMorph.prototype.toXML = function (serializer) {
    var isVar = this.target instanceof VariableFrame,
        isList = this.currentValue instanceof List,
        color = this.readoutColor,
        position = this.parent ?
                this.topLeft().subtract(this.parent.topLeft())
                : this.topLeft();
    if (this.isTemporary()) {
        // do not save watchers on temporary variables
        return '';
    }
    return serializer.format(
        '',
        (isVar && this.target.owner) || (!isVar && this.target) ?
                    serializer.format(' scope="@"',
                        isVar ? this.target.owner.name : this.target.name)
                            : '',
        serializer.format(isVar ? 'var="@"' : 's="@"', this.getter),
        this.style,
        isVar && this.style === 'slider' ? serializer.format(
                ' min="@" max="@"',
                this.sliderMorph.start,
                this.sliderMorph.stop
            ) : '',
        position.x,
        position.y,
        color.r,
        color.g,
        color.b,
        !isList ? ''
                : serializer.format(
                ' extX="@" extY="@"',
                this.cellMorph.contentsMorph.width(),
                this.cellMorph.contentsMorph.height()
            ),
        this.isVisible ? '' : ' hidden="true"'
    );
};
// Scripts
ScriptsMorph.prototype.toXML = function (serializer) {
    return this.children.reduce(function (xml, child) {
        if (child instanceof BlockMorph) {
            return xml + child.toScriptXML(serializer, true);
        }
        if (child instanceof CommentMorph && !child.block) { // unattached
            return xml + child.toXML(serializer);
        }
        return xml;
    }, '');
};
BlockMorph.prototype.toXML = BlockMorph.prototype.toScriptXML = function (
    serializer,
    savePosition
) {
    var position,
        xml,
        scale = SyntaxElementMorph.prototype.scale,
        block = this;
    // determine my position
    if (this.parent) {
        position = this.topLeft().subtract(this.parent.topLeft());
    } else {
        position = this.topLeft();
    }
    // save my position to xml
    if (savePosition) {
        xml = serializer.format(
            '';
    return xml;
};
BlockMorph.prototype.toBlockXML = function (serializer) {
    return serializer.format(
        '%%',
        this.selector,
        serializer.store(this.inputs()),
        this.comment ? this.comment.toXML(serializer) : ''
    );
};
ReporterBlockMorph.prototype.toXML = function (serializer) {
    if (this.selector === 'reportGetVar') {
        if (!this.comment) {
            return serializer.format(
                '',
                this.blockSpec);
        } else {
            return serializer.format(
                '%',
                this.blockSpec,
                this.comment.toXML(serializer));
        }
    } else {
        return this.toBlockXML(serializer);
    }
};
ReporterBlockMorph.prototype.toScriptXML = function (
    serializer,
    savePosition
) {
    var position,
        scale = SyntaxElementMorph.prototype.scale;
    // determine my save-position
    if (this.parent) {
        position = this.topLeft().subtract(this.parent.topLeft());
    } else {
        position = this.topLeft();
    }
    if (savePosition) {
        return serializer.format(
            '',
            position.x / scale,
            position.y / scale,
            this.toXML(serializer)
        );
    }
    return serializer.format('', this.toXML(serializer));
};
CustomCommandBlockMorph.prototype.toBlockXML = function (serializer) {
    var scope = this.isGlobal ? undefined : 'local';
    return serializer.format(
        '%%%',
        this.semanticSpec,
        this.isGlobal ?
                '' : serializer.format(' scope="@"', scope),
        serializer.store(this.inputs()),
        this.isGlobal &&
        	this.definition.variableNames.length &&
            !serializer.isExportingBlocksLibrary ?
                '' +
                    this.variables.toXML(serializer) +
                    ''
                        : '',
        this.comment ? this.comment.toXML(serializer) : ''
    );
};
CustomReporterBlockMorph.prototype.toBlockXML
    = CustomCommandBlockMorph.prototype.toBlockXML;
CustomBlockDefinition.prototype.toXML = function (serializer) {
    var myself = this;
    function encodeScripts(array) {
        return array.reduce(function (xml, element) {
            if (element instanceof BlockMorph) {
                return xml + element.toScriptXML(serializer, true);
            }
            if (element instanceof CommentMorph && !element.block) {
                return xml + element.toXML(serializer);
            }
            return xml;
        }, '');
    }
    return serializer.format(
        '' +
            '%' +
            (this.variableNames.length ? '%' : '@') +
            '' +
            '@' +
            '@' +
            '%%%' +
            '',
        this.spec,
        this.type,
        this.category || 'other',
        this.comment ? this.comment.toXML(serializer) : '',
        (this.variableNames.length ?
                serializer.store(new List(this.variableNames)) : ''),
        this.codeHeader || '',
        this.codeMapping || '',
        this.translationsAsText(),
        Array.from(this.declarations.keys()).reduce(function (xml, decl) {
            // to be refactored now that we've moved to ES6 Map:
                return xml + serializer.format(
                    '$%',
                    myself.declarations.get(decl)[0],
                    myself.declarations.get(decl)[3] ?
                            ' readonly="true"' : '',
                    myself.declarations.get(decl)[1],
                    myself.declarations.get(decl)[2] ?
                            serializer.format(
                                '@',
                                myself.declarations.get(decl)[2]
                            ) : ''
                );
            }, ''),
        this.body ? serializer.store(this.body.expression) : '',
        this.scripts.length > 0 ?
                    '' + encodeScripts(this.scripts) + ''
                        : ''
    );
};
// Scripts - Inputs
ArgMorph.prototype.toXML = function () {
    return ''; // empty by default
};
BooleanSlotMorph.prototype.toXML = function () {
    return (typeof this.value === 'boolean') ?
            '' + this.value + ''
                    : '';
};
InputSlotMorph.prototype.toXML = function (serializer) {
	if (this.selectedBlock) {
 		return serializer.format(
        	'@',
            this.selectedBlock.semanticSpec,
         	this.selectedBlock instanceof CommandBlockMorph ? 'command'
          		: (this.selectedBlock.isPredicate ? 'predicate' : 'reporter'),
            this.selectedBlock.category,
            this.selectedBlock.storedTranslations
        );
 	}
    if (this.constant) {
        return serializer.format(
            '',
            this.constant
        );
    }
    return serializer.format('$', this.contents().text);
};
TemplateSlotMorph.prototype.toXML = function (serializer) {
    return serializer.format('$', this.contents());
};
CommandSlotMorph.prototype.toXML = function (serializer) {
    var block = this.nestedBlock();
    if (block instanceof BlockMorph) {
        if (block instanceof ReporterBlockMorph) {
            return serializer.format(
                '%',
                serializer.store(block)
            );
        }
        return serializer.store(block);
    }
    return '';
};
FunctionSlotMorph.prototype.toXML = CommandSlotMorph.prototype.toXML;
MultiArgMorph.prototype.toXML = function (serializer) {
    return serializer.format(
        '%
',
        serializer.store(this.inputs())
    );
};
ArgLabelMorph.prototype.toXML = function (serializer) {
    return serializer.format(
        '%',
        serializer.store(this.inputs()[0])
    );
};
ColorSlotMorph.prototype.toXML = function (serializer) {
    return serializer.format(
        '$,$,$,$',
        this.color.r,
        this.color.g,
        this.color.b,
        this.color.a
    );
};
// Values
List.prototype.toXML = function (serializer, mediaContext) {
    // mediaContext is an optional name-stub
    // when collecting media into a separate module
    var xml, value, item;
    if (this.hasOnlyAtomicData() &&
            (!this.isLinked || !StageMorph.prototype.enableSublistIDs)) {
        // special case for a less cluttered format
        return serializer.format(
            '@
',
            this.asCSV()
        );
    }
    if (this.isLinked) {
        xml = '';
        if (StageMorph.prototype.enableSublistIDs) {
            // recursively nest tails:
            value = this.first;
            if (!isNil(value)) {
                xml += serializer.format(
                    '- %',
                    typeof value === 'object' ?
                            (isSnapObject(value) ? ''
                                    : serializer.store(value, mediaContext))
                            : typeof value === 'boolean' ?
                                    serializer.format('$', value)
                                    : serializer.format('$', value)
                );
            }
            if (!isNil(this.rest)) {
                xml += serializer.store(this.rest, mediaContext);
            }
            return xml + '
';
        }
        // else sequentially serialize tails:
        item = this;
        do {
            value = item.first;
            if (!isNil(value)) {
                xml += serializer.format(
                    '- %',
                    typeof value === 'object' ?
                            (isSnapObject(value) ? ''
                                    : serializer.store(value, mediaContext))
                            : typeof value === 'boolean' ?
                                    serializer.format('$', value)
                                    : serializer.format('$', value)
                );
            }
            item = item.rest;
        } while (!isNil(item));
        return xml + '';
    }
    // dynamic array:
    return serializer.format(
        '
%
',
        this.contents.reduce(function (xml, item) {
            return xml + serializer.format(
                '- %',
                typeof item === 'object' ?
                        (isSnapObject(item) ? ''
                                : serializer.store(item, mediaContext))
                        : typeof item === 'boolean' ?
                                serializer.format('$', item)
                                : serializer.format('$', item)
            );
        }, '')
    );
};
Context.prototype.toXML = function (serializer) {
    if (this.isContinuation) { // continuations are transient in Snap!
        return '';
    }
    return serializer.format(
        '%%' +
            '%%%%',
        this.inputs.reduce(
                function (xml, input) {
                    return xml + serializer.format('$', input);
                },
                ''
            ),
        this.variables ? serializer.store(this.variables) : '',
        this.expression ? serializer.store(this.expression) : '',
        this.receiver ? serializer.store(this.receiver) : '',
        this.receiver ? serializer.store(this.origin) : '',
        this.outerContext ? serializer.store(this.outerContext) : ''
    );
};
// Comments
CommentMorph.prototype.toXML = function (serializer) {
    var position,
        scale = SyntaxElementMorph.prototype.scale;
    if (this.block) { // attached to a block
        return serializer.format(
            '%',
            this.textWidth() / scale,
            this.isCollapsed,
            serializer.escape(this.text())
        );
    }
    // free-floating, determine my save-position
    if (this.parent) {
        position = this.topLeft().subtract(this.parent.topLeft());
    } else {
        position = this.topLeft();
    }
    return serializer.format(
        '%',
        position.x / scale,
        position.y / scale,
        this.textWidth() / scale,
        this.isCollapsed,
        serializer.escape(this.text())
    );
};