/**
 * The container that holds and manages instances of the {@link Ext.draw.Surface}
 * in which {@link Ext.draw.sprite.Sprite sprites} are rendered.  Draw containers are 
 * used as the foundation for all of the chart classes but may also be created directly 
 * in order to create custom drawings.
 * 
 *     @example
 *     var drawContainer = Ext.create('Ext.draw.Container', {
 *         renderTo: Ext.getBody(),
 *         width:200,
 *         height:200,
 *         sprites: [{
 *             type: 'circle',
 *             fillStyle: '#79BB3F',
 *             r: 100,
 *             x: 100,
 *             y: 100
 *          }]
 *     });
 *
 *     // Uncomment to trigger a download of the painted circle.
 *     // drawContainer.download({
 *     //     filename: 'Circle',
 *     //     url: 'http://svg.sencha.io' // Default server the image data is sent to.
 *     // });
 *
 * In the previous example we created a draw container and configured it with a single 
 * sprite.  The *type* of the sprite is {@link Ext.draw.sprite.Circle circle}, so if you 
 * run this code you'll see a green circle.
 *
 * You can attach sprite event listeners to the draw container with the help of the
 * {@link Ext.draw.plugin.SpriteEvents} plugin.
 *
 * For more information on sprites, the core elements added to a draw container's 
 * surface, refer to the Ext.draw.sprite.Sprite documentation.
 * 
 * For more information on surfaces, the interface owned by the draw container used to 
 * manage all sprites, see the Ext.draw.Surface documentation.
 */
Ext.define('Ext.draw.Container', {
    extend: 'Ext.draw.ContainerBase',
    alternateClassName: 'Ext.draw.Component',
    xtype: 'draw',
    defaultType: 'surface',
    isDrawContainer: true,
 
    requires: [
        'Ext.draw.Surface',
        'Ext.draw.engine.Svg',
        'Ext.draw.engine.Canvas',
        'Ext.draw.gradient.GradientDefinition'
    ],
    /**
     * @cfg {String} [engine="Ext.draw.engine.Canvas"]
     * Defines the engine (type of surface) used to render draw container contents.  
     * 
     * The render engine is selected automatically depending on the platform used. Priority 
     * is given to the {@link Ext.draw.engine.Canvas} engine due to its performance advantage.
     *
     * You may also set the engine config to be `Ext.draw.engine.Svg` if so desired.
     */
    engine: 'Ext.draw.engine.Canvas',
 
    /**
     * @event spritemousemove
     * Fires when the mouse is moved on a sprite.
     * @param {Object} sprite 
     * @param {Event} event 
     */
 
    /**
     * @event spritemouseup
     * Fires when a mouseup event occurs on a sprite.
     * @param {Object} sprite 
     * @param {Event} event 
     */
 
    /**
     * @event spritemousedown
     * Fires when a mousedown event occurs on a sprite.
     * @param {Object} sprite 
     * @param {Event} event 
     */
 
    /**
     * @event spritemouseover
     * Fires when the mouse enters a sprite.
     * @param {Object} sprite 
     * @param {Event} event 
     */
 
    /**
     * @event spritemouseout
     * Fires when the mouse exits a sprite.
     * @param {Object} sprite 
     * @param {Event} event 
     */
 
    /**
     * @event spriteclick
     * Fires when a click event occurs on a sprite.
     * @param {Object} sprite 
     * @param {Event} event 
     */
 
    /**
     * @event spritedblclick
     * Fires when a double click event occurs on a sprite.
     * @param {Object} sprite 
     * @param {Event} event 
     */
 
    /**
     * @event spritetap
     * Fires when a tap event occurs on a sprite.
     * @param {Object} sprite 
     * @param {Event} event 
     */
 
    /**
     * @event bodyresize
     * Fires when the size of the draw container body changes.
     * @param {Object} size The object containing 'width' and 'height' of the draw container's body.
     */
 
    config: {
        cls: [
            Ext.baseCSSPrefix + 'draw-container',
            Ext.baseCSSPrefix + 'unselectable'
        ],
 
        /**
         * @cfg {Function} [resizeHandler]
         * The resize function that can be configured to have a behavior,
         * e.g. resize draw surfaces based on new draw container dimensions.
         * The `resizeHandler` function takes a single parameter -
         * the size object with `width` and `height` properties.
         *
         * **Note:** Since resize events trigger {@link #renderFrame} calls automatically,
         * return `false` from the resize function, if it also calls `renderFrame`,
         * to prevent double rendering.
         */
        resizeHandler: null,
 
        /**
         * @cfg {Object[]} sprites
         * Defines a set of sprites to be added to the drawContainer surface.
         *
         * For example:
         *
         *      sprites: [{
         *           type: 'circle',
         *           fillStyle: '#79BB3F',
         *           r: 100,
         *           x: 100,
         *           y: 100
         *      }]
         * 
         */
        sprites: null,
 
        /**
         * @cfg {Object[]} gradients
         * Defines a set of gradients that can be used as color properties
         * (fillStyle and strokeStyle, but not shadowColor) in sprites.
         * The gradients array is an array of objects with the following properties:
         * - **id** - string - The unique name of the gradient.
         * - **type** - string, optional - The type of the gradient. Available types are: 'linear',
         * 'radial'. Defaults to 'linear'.
         * - **angle** - number, optional - The angle of the gradient in degrees.
         * - **stops** - array - An array of objects with 'color' and 'offset' properties, where
         * 'offset' is a real number from 0 to 1.
         *
         * For example:
         *
         *     gradients: [{
         *         id: 'gradientId1',
         *         type: 'linear',
         *         angle: 45,
         *         stops: [{
         *             offset: 0,
         *             color: 'red'
         *         }, {
         *            offset: 1,
         *            color: 'yellow'
         *         }]
         *     }, {
         *        id: 'gradientId2',
         *        type: 'radial',
         *        stops: [{
         *            offset: 0,
         *            color: '#555',
         *        }, {
         *            offset: 1,
         *            color: '#ddd',
         *        }]
         *     }]
         *
         * Then the sprites can use 'gradientId1' and 'gradientId2' by setting the color attributes
         * to those ids, for example:
         *
         *     sprite.setAttributes({
         *         fillStyle: 'url(#gradientId1)',
         *         strokeStyle: 'url(#gradientId2)'
         *     });
         */
        gradients: [],
 
        /**
         * @cfg {String} downloadServerUrl
         * The default URL used by the {@link #download} method.
         */
        downloadServerUrl: undefined,
 
        touchAction: {
            panX: false,
            panY: false,
            pinchZoom: false,
            doubleTapZoom: false
        },
 
        /**
         * @private
         * @cfg {Object} surfaceZIndexes A map of surface type name to zIndex.
         * The z-indexes to use for the various types of surfaces.
         */
        surfaceZIndexes: {
            main: 1
        }
    },
 
    /**
     * @private
     * @property {String} [defaultDownloadServerUrl="http://svg.sencha.io"]
     * The default URL used by the {@link #download} method if the {@link #downloadServerUrl}
     * config wasn't set.
     * To override this globally, set the `Ext.draw.Container.prototype.defaultDownloadServerUrl`.
     *
     * **Note:** Downloading charts via Sencha's http://svg.sencha.io server may expose
     * your data. This server will be deprecated (turned down) in a future SDK release. Please
     * implement your own server to provide support for older browsers that do not support file
     * saving via blob. An example implementation can be seen inside the Charts Package source:
     * packages/charts/server/node
     */
    defaultDownloadServerUrl: 'http://svg.sencha.io',
 
    /**
     * @property {Array} [supportedFormats=["png", "pdf", "jpeg", "gif"]]
     * A list of export types supported by the server.
     * @private
     */
    supportedFormats: ['png', 'pdf', 'jpeg', 'gif'],
 
    supportedOptions: {
        version: Ext.isNumber,
        data: Ext.isString,
        format: function(format) {
            return Ext.Array.indexOf(this.supportedFormats, format) >= 0;
        },
        filename: Ext.isString,
        width: Ext.isNumber,
        height: Ext.isNumber,
        scale: Ext.isNumber,
        pdf: Ext.isObject,
        jpeg: Ext.isObject
    },
 
    initAnimator: function() {
        this.frameCallbackId = Ext.draw.Animator.addFrameCallback('renderFrame', this);
    },
 
    applyDownloadServerUrl: function(url) {
        var defaultUrl = this.defaultDownloadServerUrl;
 
        if (!url) {
            url = defaultUrl;
 
            //<debug>
            // Skip this warning when unit testing.
            if (!window.jasmine) {
                Ext.log.warn('Downloading charts via Sencha\'http://svg.sencha.io server may ' +
                            'expose your data. This server will be deprecated (turned down) in ' +
                            'a future SDK release. Please implement your own server to provide ' +
                            'support for older browsers that do not support file saving via ' +
                            'blob. An example implementation can be seen inside the Charts ' +
                            'Package source: packages/charts/server/node.');
            }
            //</debug>
        }
 
        return url;
    },
 
    applyGradients: function(gradients) {
        var result = [],
            i, n, gradient, offset;
 
        if (!Ext.isArray(gradients)) {
            return result;
        }
 
        for (= 0, n = gradients.length; i < n; i++) {
            gradient = gradients[i];
 
            if (!Ext.isObject(gradient)) {
                continue;
            }
 
            // ExtJS only supported linear gradients, so we didn't have to specify their type
            if (typeof gradient.type !== 'string') {
                gradient.type = 'linear';
            }
 
            if (gradient.angle) {
                gradient.degrees = gradient.angle;
                delete gradient.angle;
            }
 
            // Convert ExtJS stops object to Touch stops array
            if (Ext.isObject(gradient.stops)) {
                gradient.stops = (function(stops) {
                    var result = [],
                        stop;
 
                    for (offset in stops) {
                        stop = stops[offset];
                        stop.offset = offset / 100;
                        result.push(stop);
                    }
 
                    return result;
                })(gradient.stops);
            }
 
            result.push(gradient);
        }
 
        Ext.draw.gradient.GradientDefinition.add(result);
 
        return result;
    },
 
    applySprites: function(sprites) {
        var result, surface, sprite, i, ln;
 
        // Never update.
        if (!sprites) {
            return;
        }
 
        sprites = Ext.Array.from(sprites);
        result = [];
 
        for (= 0, ln = sprites.length; i < ln; i++) {
            sprite = sprites[i];
            surface = sprite.surface;
 
            if (!(surface && surface.isSurface)) {
                if (Ext.isString(surface)) {
                    surface = this.getSurface(surface);
                    delete sprite.surface;
                }
                else {
                    surface = this.getSurface('main');
                }
            }
 
            sprite = surface.add(sprite);
            result.push(sprite);
        }
 
        return result;
    },
 
    resizeDelay: 500, // in milliseconds
    resizeTimerId: 0,
    lastResizeTime: null,
 
    /**
     * @private
     * @property
     * Last valid size.
     */
    size: null,
 
    /**
     * Triggers the {@link #resizeHandler} with the size of the draw container
     * element as the parameter.
     */
    handleResize: function(size, instantly) {
        // See the following:
        // Classic: Ext.draw.ContainerBase.reattachToBody
        //  Modern: Ext.draw.ContainerBase.initialize
        var me = this,
            el = me.element,
            resizeHandler = me.getResizeHandler() || me.defaultResizeHandler,
            resizeDelay = me.resizeDelay,
            lastResizeTime = me.lastResizeTime,
            defer, result;
 
        if (!el) {
            return;
        }
 
        size = size || el.getSize();
 
        if (!(size.width && size.height)) {
            return;
        }
 
        me.size = size;
 
        me.stopResizeTimer();
 
        // Only want to defer when multiple resize events happen in quick succession.
        // That way it doesn't feel luggy during an occasional resize, nor it's too straining
        // when continuously resizing.
        defer = !instantly && lastResizeTime && (Ext.Date.now() - lastResizeTime < resizeDelay);
 
        if (defer) {
            me.resizeTimerId = Ext.defer(me.handleResize, resizeDelay, me, [size, true]);
 
            return;
        }
 
        me.fireEvent('bodyresize', me, size);
 
        Ext.callback(resizeHandler, null, [size], 0, me);
 
        if (result !== false) {
            me.renderFrame();
        }
 
        me.lastResizeTime = Ext.Date.now();
    },
 
    /**
     * @private
     */
    stopResizeTimer: function() {
        if (this.resizeTimerId) {
            Ext.undefer(this.resizeTimerId);
            this.resizeTimerId = 0;
        }
    },
 
    defaultResizeHandler: function(size) {
        this.getItems().each(function(surface) {
            surface.setRect([0, 0, size.width, size.height]);
        });
    },
 
    /**
     * Get a surface by the given id or create one if it doesn't exist.
     * This will automatically call the {@link #resizeHandler}. Which
     * means that, if no custom resize handler has been provided, the
     * surface will be sized to match the container.
     * If the {@link #method!add} method is used, it is the responsibility
     * of the user to call the {@link #handleResize} method, to update
     * the size of all added surfaces.
     * @param {String} [id="main"] 
     * @param {String} type 
     * @return {Ext.draw.Surface} 
     */
    getSurface: function(id, type) {
        var me = this,
            surfaces = me.getItems(),
            oldCount = surfaces.getCount(),
            zIndexes = me.getSurfaceZIndexes(),
            surface;
 
        id = id || 'main';
        type = type || id;
 
        surface = me.createSurface(id);
 
        if (type in zIndexes) {
            surface.element.setStyle('zIndex', zIndexes[type]);
        }
 
        if (surfaces.getCount() > oldCount) {
            // Immediately call resize handler of the draw container,
            // so that the newly created surface gets a size.
            me.handleResize(null, true);
        }
 
        return surface;
    },
 
    createSurface: function(id) {
        var me = this,
            surfaces = me.getItems(),
            surface;
 
        id = this.getId() + '-' + (id || 'main');
        surface = surfaces.get(id);
 
        if (!surface) {
            surface = me.add({ xclass: me.engine, id: id });
        }
 
        return surface;
    },
 
    /**
     * Render all the surfaces in the container.
     */
    renderFrame: function() {
        var me = this,
            surfaces = me.getItems(),
            i, ln, item;
 
        for (= 0, ln = surfaces.length; i < ln; i++) {
            item = surfaces.items[i];
 
            if (item.isSurface) {
                item.renderFrame();
            }
        }
    },
 
    /**
     * @private
     * Returns a slice of the surfaces (items) array of the draw container,
     * optionally sorting them by zIndex.
     * Overridden in subclasses.
     */
    getSurfaces: function(sort) {
        var surfaces = Array.prototype.slice.call(this.items.items),
            zIndexes = this.getSurfaceZIndexes(),
            i, j, surface, zIndex;
 
        if (sort) {
            // Sort the surfaces by zIndex using insertion sort.
            for (= 1; j < surfaces.length; j++) {
                surface = surfaces[j];
                zIndex = zIndexes[surface.type];
                i = j - 1;
 
                while (>= 0 && zIndexes[surfaces[i].type] > zIndex) {
                    surfaces[+ 1] = surfaces[i];
                    i--;
                }
 
                surfaces[+ 1] = surface;
            }
        }
 
        return surfaces;
    },
 
    /**
     * Produces an image of the chart / drawing.
     * @param {String} [format] Possible options are 'image' (the method will return an 
     * Image object) and 'stream' (the method will return the image as a byte stream).  
     * If missing, the data URI of the drawing's (or chart's) image will be returned.
     * Note: for an SVG based drawing/chart in IE/Edge browsers the method will always
     * return SVG markup instead of a data URI, as 'img' elements won't accept a data
     * URI anyway in those browsers.
     * @return {Object} 
     * @return {String} return.data Image element, byte stream or DataURL.
     * @return {String} return.type The type of the data (e.g. 'png' or 'svg').
     */
    getImage: function(format) {
        var size = this.bodyElement.getSize(),
            surfaces = this.getSurfaces(true),
            surface = surfaces[0],
            image, imageElement;
 
        if ((Ext.isIE || Ext.isEdge) && surface.isSVG) {
            // SVG data URLs don't work in IE/Edge as a source for an 'img' element,
            // so we need to render SVG the usual way.
            image = {
                data: surface.toSVG(size, surfaces),
                type: 'svg-markup'
            };
        }
        else {
            image = surface.flatten(size, surfaces);
 
            if (format === 'image') {
                imageElement = new Image();
                imageElement.src = image.data;
                image.data = imageElement;
 
                return image;
            }
 
            if (format === 'stream') {
                image.data = image.data.replace(/^data:image\/[^;]+/, 'data:application/octet-stream');
 
                return image;
            }
        }
 
        return image;
    },
 
    /**
     * Downloads an image or PDF of the chart / drawing or opens it in a separate 
     * browser tab/window if the download can't be triggered. The exact behavior is
     * platform and browser specific. For more consistent results on mobile devices use
     * the {@link #preview} method instead. This method doesn't work in IE8.
     *
     * Important: The default download mechanism sends image data to `http://svg.sencha.io`,
     * which is a server operated by Sencha. This can be changed by setting
     * the {@link #downloadServerUrl} config to the address of another server.
     *
     * You can deploy your own server by using the code from the `server` directory
     * in the Charts package. The server is Node.js based and uses PhantomJS to
     * generate images and PDFs from received data.
     *
     * The warning that the default download server is used can be suppressed
     * by explicitly setting the value of the {@link #downloadServerUrl} config
     * to `http://svg.sencha.io`.
     *
     * @param {Object} [config] The following config options are supported:
     *
     * @param {String} config.url The url to post the data to. Defaults to
     * the value of the {@link #downloadServerUrl} config.
     *
     * @param {String} config.format The format of image to export. See the
     * {@link #supportedFormats}. Defaults to 'png' on the Sencha IO server.
     * Note that you can't export to 'svg' format if the {@link Ext.draw.engine.Canvas Canvas}
     * {@link Ext.draw.Container#engine engine} is used.
     *
     * @param {Number} config.width A width to send to the server for
     * configuring the image width. Defaults to natural image width on
     * the Sencha IO server.
     *
     * @param {Number} config.height A height to send to the server for
     * configuring the image height. Defaults to natural image height on
     * the Sencha IO server.
     *
     * @param {String} config.filename The filename of the downloaded image.
     * Defaults to 'chart' on the Sencha IO server. The config.format is used
     * as a filename extension.
     *
     * @param {Number} config.scale The scaling of the downloaded image.
     * Defaults to 1 on the Sencha IO server. The server will try to determine the natural
     * size of the image unless the width/height configs have been set. If the
     * {@link Ext.draw.engine.Canvas Canvas} {@link Ext.draw.Container#engine engine} is
     * used the natural image size will depend on the value of the window.devicePixelRatio.
     * For example, for devices with devicePixelRatio of 2 the produced image will be
     * two times larger than for devices with devicePixelRatio of 1 for the same drawing.
     * This is done so that the users with devices with HiDPI screens get a downloaded
     * image that looks as crisp on their device as the original drawing.
     * If you want image size to be consistent across devices with different device
     * pixel ratios, you can set the value of this config to 1/devicePixelRatio.
     * This parameter is ignored by the Sencha IO server if config.format is set to 'svg'.
     *
     * @param {Object} config.pdf PDF specific options.
     * This config is only used if config.format is set to 'pdf'.
     * The given object should be in either this format:
     *
     *     {
     *       width: '200px',
     *       height: '300px',
     *       border: '0px'
     *     }
     *
     * or this format:
     *
     *     {
     *       format: 'A4',
     *       orientation: 'portrait',
     *       border: '1cm'
     *     }
     *
     * Supported dimension units are: 'mm', 'cm', 'in', 'px'. No unit means 'px'.
     * Supported formats are: 'A3', 'A4', 'A5', 'Legal', 'Letter', 'Tabloid'.
     * Orientation ('portrait', 'landscape') is optional and defaults to 'portrait'.
     *
     * @param {Object} config.jpeg JPEG specific options.
     * This config is only used if config.format is set to 'jpeg'.
     * The given object should be in this format:
     *
     *     {
     *       quality: 80
     *     }
     *
     * Where quality is an integer between 0 and 100.
     *
     * @return {Boolean} True if request was successfully sent to the server.
     */
    download: function(config) {
        var me = this,
            inputs = [],
            markup, name, value;
 
        if (Ext.isIE8) {
            return false;
        }
 
        config = config || {};
        config.version = 2;
 
        if (!config.data) {
            config.data = me.getImage().data;
        }
 
        for (name in config) {
            if (config.hasOwnProperty(name)) {
                value = config[name];
 
                if (name in me.supportedOptions) {
                    if (me.supportedOptions[name].call(me, value)) {
                        inputs.push({
                            tag: 'input',
                            type: 'hidden',
                            name: name,
                            value: Ext.String.htmlEncode(
                                Ext.isObject(value) ? Ext.JSON.encode(value) : value
                            )
                        });
                    }
                    //<debug>
                    else {
                        Ext.log.error('Invalid value for image download option "' + name +
                                      '": ' + value);
                    }
                    //</debug>
                }
                //<debug>
                else {
                    Ext.log.error('Invalid image download option: "' + name + '"');
                }
                //</debug>
            }
        }
 
        markup = Ext.dom.Helper.markup({
            tag: 'html',
            children: [
                { tag: 'head' },
                {
                    tag: 'body',
                    children: [
                        {
                            tag: 'form',
                            method: 'POST',
                            action: config.url || me.getDownloadServerUrl(),
                            children: inputs
                        },
                        {
                            tag: 'script',
                            type: 'text/javascript',
                            children: 'document.getElementsByTagName("form")[0].submit();'
                        }
                    ]
                }
            ]
        });
 
        window.open('', 'ImageDownload_' + Date.now()).document.write(markup);
    },
 
    /**
     * @method preview
     * Displays an image of a Ext.draw.Container on screen.
     * On mobile devices this lets users tap-and-hold to bring up the menu
     * with image saving options.
     * Notes:
     * - some browsers won't save the preview image if it's SVG based
     *   (i.e. generated from a draw container that uses 'Ext.draw.engine.Svg' engine);
     * - some platforms may not have the means of viewing successfully saved SVG images;
     * - this method does not work on IE8.
     */
 
    doDestroy: function() {
        var me = this,
            callbackId = me.frameCallbackId;
 
        if (callbackId) {
            Ext.draw.Animator.removeFrameCallback(callbackId);
        }
 
        me.stopResizeTimer();
 
        me.callParent();
    }
 
}, function() {
    if (location.search.match('svg')) {
        Ext.draw.Container.prototype.engine = 'Ext.draw.engine.Svg';
    }
    else if ((Ext.os.is.BlackBerry && Ext.os.version.getMajor() === 10) ||
             (Ext.browser.is.AndroidStock4 && (Ext.os.version.getMinor() === 1 ||
             Ext.os.version.getMinor() === 2 || Ext.os.version.getMinor() === 3))) {
        // http://code.google.com/p/android/issues/detail?id=37529
        Ext.draw.Container.prototype.engine = 'Ext.draw.engine.Svg';
    }
});