/**
 * サーフェスは、{@link Ext.draw.Container}内でメソッドを描画するインターフェイスです。サーフェスには、スプライトを描画する、スプライトのバウンディングボックスを取得する、スプライトをキャンバスに追加する、その他のグラフィックコンポーネントを初期化するなど、さまざまなメソッドが含まれています。このクラスで最も使用されるメソッドの1つは、スプライトをサーフェスに追加する`add`メソッドです。
 *
 * サーフェスメソッドのほとんどは抽象的であり、キャンバスまたはSVGエンジン内での実装で具体化します。
 *
 * サーフェスインスタンスには、描画コンテナのプロパティとしてアクセスできます。例:
 *
 *     drawContainer.getSurface('main').add({
 *         type: 'circle',
 *         fill: '#ffc',
 *         radius: 100,
 *         x: 100,
 *         y: 100
 *     });
 *
 * `add`メソッド内で渡される設定オブジェクトは、{@link Ext.draw.sprite.Sprite}クラスのドキュメンテーションで述べられているものと同じです。
 *
 * ##例
 *
 *     drawContainer.getSurface('main').add([
 *         {
 *             type: 'circle',
 *             radius: 10,
 *             fill: '#f00',
 *             x: 10,
 *             y: 10
 *         },
 *         {
 *             type: 'circle',
 *             radius: 10,
 *             fill: '#0f0',
 *             x: 50,
 *             y: 50
 *         },
 *         {
 *             type: 'circle',
 *             radius: 10,
 *             fill: '#00f',
 *             x: 100,
 *             y: 100
 *         },
 *         {
 *             type: 'rect',
 *             radius: 10,
 *             x: 10,
 *             y: 10
 *         },
 *         {
 *             type: 'rect',
 *             radius: 10,
 *             x: 50,
 *             y: 50
 *         },
 *         {
 *             type: 'rect',
 *             radius: 10,
 *             x: 100,
 *             y: 100
 *         }
 *     ]);
 *
 */
Ext.define('Ext.draw.Surface', {
    extend: 'Ext.draw.SurfaceBase',
    xtype: 'surface',

    requires: [
        'Ext.draw.sprite.*',
        'Ext.draw.gradient.*',
        'Ext.draw.sprite.AttributeDefinition',
        'Ext.draw.Matrix',
        'Ext.draw.Draw'
    ],

    uses: [
        'Ext.draw.engine.Canvas'
    ],

    /**
     * ピクセル密度を報告されているデバイス。
     */
    devicePixelRatio: window.devicePixelRatio || 1,

    statics: {
        /**
         * zIndexによるスプライトリストの確実なソート。やるべきこと:性能を向上させる。GCの影響を減らす。
         * @param {Array} list
         */
        stableSort: function (list) {
            if (list.length < 2) {
                return;
            }
            var keys = {}, sortedKeys, result = [], i, ln, zIndex;

            for (i = 0, ln = list.length; i < ln; i++) {
                zIndex = list[i].attr.zIndex;
                if (!keys[zIndex]) {
                    keys[zIndex] = [list[i]];
                } else {
                    keys[zIndex].push(list[i]);
                }
            }
            sortedKeys = Ext.Object.getKeys(keys).sort(function (a, b) {return a - b;});
            for (i = 0, ln = sortedKeys.length; i < ln; i++) {
                result.push.apply(result, keys[sortedKeys[i]]);
            }
            for (i = 0, ln = list.length; i < ln; i++) {
                list[i] = result[i];
            }
        }
    },

    config: {
        cls: Ext.baseCSSPrefix + 'surface',
        /**
         * @cfg {Array}
         * コンテナに関連するサーフェスの矩形。
         */
        rect: null,

        /**
         * @cfg {Object}
         * サーフェスの背景スプライトコンフィグ。
         */
        background: null,

        /**
         * @cfg {Array}
         * スプライトインスタンスの配列。
         */
        items: [],

        /**
         * @cfg {Boolean}
         * サーフェスに再描画が必要かどうかを示します。
         */
        dirty: false,

        /**
         * @cfg {Boolean} flipRtlText
         * サーフェスがRTLモードの場合、テキストはRTL方向で描画されますが、テキストの位置揃えと場所はデフォルトでは変わりません。このコンフィグに'true'をセットすると、サーフェス内におけるテキストの位置揃えと場所がミラーリングされます。
         */
        flipRtlText: false
    },

    isSurface: true,

    dirtyPredecessor: 0,

    constructor: function (config) {
        var me = this;

        me.predecessors = [];
        me.successors = [];
        // The `pendingRenderFrame` flag is used to indicate that `predecessors` (surfaces that should render first)
        // are dirty, and to call `renderFrame` when all `predecessors` have their `renderFrame` called
        // (i.e. not dirty anymore).
        me.pendingRenderFrame = false;
        me.map = {};

        me.callParent([config]);
        me.matrix = new Ext.draw.Matrix();
        me.inverseMatrix = me.matrix.inverse(me.inverseMatrix);
        me.resetTransform();
    },

    /**
     * デバイスのピクセル数に揃えるために値を丸めます。
     * @param {Number} num 揃える値。
     * @return {Number} 揃えた結果。
     */
    roundPixel: function (num) {
        return Math.round(this.devicePixelRatio * num) / this.devicePixelRatio;
    },

    /**
     * 別のサーフェスが更新された後に、描画するサーフェスをマークします。
     * @param {Ext.draw.Surface} surface 待機中のサーフェス。
     */
    waitFor: function (surface) {
        var me = this,
            predecessors = me.predecessors;
        if (!Ext.Array.contains(predecessors, surface)) {
            predecessors.push(surface);
            surface.successors.push(me);
            if (surface._dirty) {
                me.dirtyPredecessor++;
            }
        }
    },

    setDirty: function (dirty) {
        if (this._dirty !== dirty) {
            var successors = this.successors, successor,
                i, ln = successors.length;
            for (i = 0; i < ln; i++) {
                successor = successors[i];
                if (dirty) {
                    successor.dirtyPredecessor++;
                    successor.setDirty(true);
                } else {
                    successor.dirtyPredecessor--;
                    if (successor.dirtyPredecessor === 0 && successor.pendingRenderFrame) {
                        successor.renderFrame();
                    }
                }
            }
            this._dirty = dirty;
        }
    },

    applyElement: function (newElement, oldElement) {
        if (oldElement) {
            oldElement.set(newElement);
        } else {
            oldElement = Ext.Element.create(newElement);
        }
        this.setDirty(true);
        return oldElement;
    },

    applyBackground: function (background, oldBackground) {
        this.setDirty(true);
        if (Ext.isString(background)) {
            background = { fillStyle: background };
        }
        return Ext.factory(background, Ext.draw.sprite.Rect, oldBackground);
    },

    applyRect: function (rect, oldRect) {
        if (oldRect && rect[0] === oldRect[0] && rect[1] === oldRect[1] && rect[2] === oldRect[2] && rect[3] === oldRect[3]) {
            return;
        }
        if (Ext.isArray(rect)) {
            return [rect[0], rect[1], rect[2], rect[3]];
        } else if (Ext.isObject(rect)) {
            return [
                rect.x || rect.left,
                rect.y || rect.top,
                rect.width || (rect.right - rect.left),
                rect.height || (rect.bottom - rect.top)
            ];
        }
    },

    updateRect: function (rect) {
        var me = this,
            l = rect[0],
            t = rect[1],
            r = l + rect[2],
            b = t + rect[3],
            background = me.getBackground(),
            element = me.element;

        element.setLocalXY(Math.floor(l), Math.floor(t));
        element.setSize(Math.ceil(r - Math.floor(l)), Math.ceil(b - Math.floor(t)));

        if (background) {
            background.setAttributes({
                x: 0,
                y: 0,
                width: Math.ceil(r - Math.floor(l)),
                height: Math.ceil(b - Math.floor(t))
            });
        }
        me.setDirty(true);
    },

    /**
     * サーフェスのマトリクスをリセットします。
     */
    resetTransform: function () {
        this.matrix.set(1, 0, 0, 1, 0, 0);
        this.inverseMatrix.set(1, 0, 0, 1, 0, 0);
        this.setDirty(true);
    },

    /**
     * IDまたはインデックスでスプライトを取得します。まず指定したIDを持つスプライトを探し、そうでなければIDをインデックスとして使用します。
     * @param {String|Number} id
     * @returns {Ext.draw.sprite.Sprite}
     */
    get: function (id) {
        return this.map[id] || this.items[id];
    },

    /**
     * サーフェスへスプライトを追加します。オブジェクトの数(任意)をパラメータとして入力することができます。このメソッドに渡すための設定オブジェクトについては、{@link Ext.draw.sprite.Sprite}を参照してください。
     *
     * 例えば、
     *
     *     drawContainer.surface.add({
     *         type: 'circle',
     *         fill: '#ffc',
     *         radius: 100,
     *         x: 100,
     *         y: 100
     *     });
     *
     */
    add: function () {
        var me = this,
            args = Array.prototype.slice.call(arguments),
            argIsArray = Ext.isArray(args[0]),
            results = [],
            sprite, sprites, items, i, ln;

        items = Ext.Array.clean(argIsArray ? args[0] : args);
        if (!items.length) {
            return results;
        }
        sprites = me.prepareItems(items);

        for (i = 0, ln = sprites.length; i < ln; i++) {
            sprite = sprites[i];
            me.map[sprite.getId()] = sprite;
            results.push(sprite);
            sprite.setParent(me);
            sprite.setSurface(me);
            me.onAdd(sprite);
        }

        items = me.getItems();
        if (items) {
            items.push.apply(items, results);
        }

        me.dirtyZIndex = true;
        me.setDirty(true);

        if (!argIsArray && results.length === 1) {
            return results[0];
        } else {
            return results;
        }
    },

    /**
     * @protected
     * サーフェスにスプライトを追加すると起動されます。
     * @param {Ext.draw.sprite.Sprite} sprite 追加されるスプライト。
     */
    onAdd: Ext.emptyFn,

    /**
     * 指定したスプライトをサーフェスから削除し、オプションでプロセス中のスプライトを廃棄します。スプライト自身の`remove`メソッドを呼び出すことも出来ます。
     *
     * 例えば、
     *
     *      drawContainer.surface.remove(sprite);
     *      // or...
     *      sprite.remove();
     *
     * @param {Ext.draw.sprite.Sprite} sprite
     * @param {Boolean} [destroySprite=false]
     */
    remove: function (sprite, destroySprite) {
        if (sprite) {
            delete this.map[sprite.getId()];
            if (destroySprite) {
                sprite.destroy();
            } else {
                sprite.setParent(null);
                sprite.setSurface(null);
                Ext.Array.remove(this.getItems(), sprite);
            }
            this.dirtyZIndex = true;
            this.setDirty(true);
        }
    },

    /**
     * サーフェスから全てのスプライトを削除し、オプションでプロセス中からスプライトを廃棄します。
     *
     * 例えば、
     *
     *      drawContainer.getSurface('main').removeAll();
     *
     * @param {Boolean} [destroySprites=false]
     */
    removeAll: function (destroySprites) {
        var items = this.getItems(),
            i = items.length,
            item;
        if (destroySprites) {
            while (i > 0) {
                items[--i].destroy();
            }
        } else {
            while (i > 0) {
                i--;
                item = items[i];
                item.setParent(null);
                item.setSurface(null);
            }
        }
        items.length = 0;
        this.map = {};
        this.dirtyZIndex = true;
    },

    // @private
    applyItems: function (items) {
        if (this.getItems()) {
            this.removeAll(true);
        }
        return Ext.Array.from(this.add(items));
    },

    /**
     * @private
     * サーフェスアイテムを初期化し、デフォルトに戻します。
     */
    prepareItems: function (items) {
        items = [].concat(items);
        // Make sure defaults are applied and item is initialized

        var me = this,
            item, i, ln, j,
            removeSprite = function (sprite) {
                this.remove(sprite, false);
            };

        for (i = 0, ln = items.length; i < ln; i++) {
            item = items[i];
            if (!(item instanceof Ext.draw.sprite.Sprite)) {
                // Temporary, just take in configs...
                item = items[i] = me.createItem(item);
            }
            item.on('beforedestroy', removeSprite, me);
        }
        return items;
    },

    /**
     * @private アイテムを作成し、そのアイテムをサーフェスに付与します。呼ばれる
     * `add`を呼び出すときに内部メソッドとして呼び出されます。
     */
    createItem: function (config) {
        return Ext.create(config.xclass || 'sprite.' + config.type, config);
    },

    /**
     * 指定した一連のスプライトのすべてのスプライトバウンディングボックスを持つ最小のバウンディングボックスを返します。
     * @param {Ext.draw.sprite.Sprite[]|Ext.draw.sprite.Sprite} sprites
     * @param {Boolean} [isWithoutTransform=false]
     * @returns {{x: Number, y: Number, width: number, height: number}}
     */
    getBBox: function (sprites, isWithoutTransform) {
        var sprites = Ext.Array.from(sprites),
            left = Infinity,
            right = -Infinity,
            top = Infinity,
            bottom = -Infinity,
            sprite, bbox, i, ln;

        for (i = 0, ln = sprites.length; i < ln; i++) {
            sprite = sprites[i];
            bbox = sprite.getBBox(isWithoutTransform);
            if (left > bbox.x) {
                left = bbox.x;
            }
            if (right < bbox.x + bbox.width) {
                right = bbox.x + bbox.width;
            }
            if (top > bbox.y) {
                top = bbox.y;
            }
            if (bottom < bbox.y + bbox.height) {
                bottom = bbox.y + bbox.height;
            }
        }
        return {
            x: left,
            y: top,
            width: right - left,
            height: bottom - top
        };
    },

    emptyRect: [0, 0, 0, 0],

    // Converts event's page coordinates into surface coordinates.
    // Note: surface's x-coordinates always go LTR, regardless of RTL mode.
    getEventXY: function (e) {
        var me = this,
            isRtl = me.getInherited().rtl,
            pageXY = e.getXY(), // Event position in page coordinates.
            container = me.el.up(),
            xy = container.getXY(), // Surface container position in page coordinates.
            rect = me.getRect() || me.emptyRect, // Surface position in surface container coordinates (LTR).
            result = [],
            width;

        if (isRtl) {
            width = container.getWidth();
            // The line below is actually a simplified form of
            // rect[2] - (pageXY[0] - xy[0] - (width - (rect[0] + rect[2]))).
            result[0] = xy[0] - pageXY[0] - rect[0] + width;
        } else {
            result[0] = pageXY[0] - xy[0] - rect[0];
        }
        result[1] = pageXY[1] - xy[1] - rect[1];
        return result;
    },

    /**
     * サーフェスコンテンツを(スプライトを処理することなく)空にします。
     */
    clear: Ext.emptyFn,

    /**
     * @private
     * 前回ソートして以来、変更したものがあった場合、z-indexの順序でアイテムを並べます。
     */
    orderByZIndex: function () {
        var me = this,
            items = me.getItems(),
            dirtyZIndex = false,
            i, ln;

        if (me.getDirty()) {
            for (i = 0, ln = items.length; i < ln; i++) {
                if (items[i].attr.dirtyZIndex) {
                    dirtyZIndex = true;
                    break;
                }
            }
            if (dirtyZIndex) {
                // sort by zIndex
                Ext.draw.Surface.stableSort(items);
                this.setDirty(true);
            }

            for (i = 0, ln = items.length; i < ln; i++) {
                items[i].attr.dirtyZIndex = false;
            }
        }
    },

    /**
     * 要素を強制的に再描画させます。
     */
    repaint: function () {
        var me = this;
        me.repaint = Ext.emptyFn;
        Ext.defer(function () {
            delete me.repaint;
            me.element.repaint();
        }, 1);
    },

    /**
     * キャンバスの再描画を開始します。
     */
    renderFrame: function () {
        if (!this.element) {
            return;
        }
        if (this.dirtyPredecessor > 0) {
            this.pendingRenderFrame = true;
            return;
        }

        var me = this,
            rect = this.getRect(),
            background = me.getBackground(),
            items = me.getItems(),
            item, i, ln;

        // Cannot render before the surface is placed.
        if (!rect) {
            return;
        }

        // This will also check the dirty flags of the sprites.
        me.orderByZIndex();
        if (me.getDirty()) {
            me.clear();
            me.clearTransform();

            if (background) {
                me.renderSprite(background);
            }

            for (i = 0, ln = items.length; i < ln; i++) {
                item = items[i];
                if (false === me.renderSprite(item)) {
                    return;
                }
                item.attr.textPositionCount = me.textPosition;
            }

            me.setDirty(false);
        }
    },

    /**
     * @private
     * 単一のスプライトをサーフェスに描画します。`renderFrame`メソッドの外側から呼び出さないでください。
     *
     * @param {Ext.draw.sprite.Sprite} sprite 描画されるスプライト。
     * @return {Boolean} 描画の継続を停止する場合は、`false`を返します。
     */
    renderSprite: Ext.emptyFn,

    /**
     * @method flatten
     * 指定した描画サーフェスを単一の画像に結合し、この画像のデータ(DataURLフォーマット)とタイプ('png'または'svg'など)を保持するオブジェクトコンテナを返します。
     * @param {Object} size 最終的な画像のサイズ。
     * @param {Number} size.width
     * @param {Number} size.height
     * @param {Ext.draw.Surface[]} surfaces 結合するサーフェス。
     * @return {Object}

     * @return {String} return.data 結合される画像のDataURL。
     * @return {String} return.type 画像の種類。
     *
     */

    /**
     * @private
     * サーフェス上にある現在の変換状態を消去します。
     */
    clearTransform: Ext.emptyFn,

    /**
     * サーフェスがダーティーな場合、trueを返します。
     * @return {Boolean} サーフェスがダーティーな場合、trueを返します
     */
    getDirty: function () {
        return this._dirty;
    },

    /**
     * サーフェスを廃棄します。この処理は全てのコンポーネントがここから削除され、 またそのDOM要素への参照が削除されることによって完了します。
     *
     * 例:
     *
     *      drawContainer.surface.destroy();
     */
    destroy: function () {
        var me = this;
        me.removeAll();
        me.setBackground(null);
        me.predecessors = null;
        me.successors = null;
        me.callParent();
    }
});