/* Copyright 2014 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        define('pdfjs/display/pattern_helper', ['exports', 'pdfjs/shared/util',
            'pdfjs/display/webgl'], factory);
    } else if (typeof exports !== 'undefined') {
        factory(exports, require('../shared/util.js'), require('./webgl.js'));
    } else {
        factory((root.pdfjsDisplayPatternHelper = {}), root.pdfjsSharedUtil,
            root.pdfjsDisplayWebGL);
    }
}(this, function(exports, sharedUtil, displayWebGL) {

    var Util = sharedUtil.Util;
    var info = sharedUtil.info;
    var isArray = sharedUtil.isArray;
    var error = sharedUtil.error;
    var WebGLUtils = displayWebGL.WebGLUtils;

    var ShadingIRs = {};

    ShadingIRs.RadialAxial = {
        fromIR : function RadialAxial_fromIR(raw) {
            var type = raw[1];
            var colorStops = raw[2];
            var p0 = raw[3];
            var p1 = raw[4];
            var r0 = raw[5];
            var r1 = raw[6];
            return {
                type       : 'Pattern',
                getPattern : function RadialAxial_getPattern(ctx) {
                    var grad;
                    if (type === 'axial') {
                        grad = ctx.createLinearGradient(p0[0], p0[1], p1[0], p1[1]);
                    } else if (type === 'radial') {
                        grad = ctx.createRadialGradient(p0[0], p0[1], r0, p1[0], p1[1], r1);
                    }

                    for (var i = 0, ii = colorStops.length; i < ii; ++i) {
                        var c = colorStops[i];
                        grad.addColorStop(c[0], c[1]);
                    }
                    return grad;
                }
            };
        }
    };

    var createMeshCanvas = (function createMeshCanvasClosure() {
        function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) {
            // Very basic Gouraud-shaded triangle rasterization algorithm.
            var coords = context.coords, colors = context.colors;
            var bytes = data.data, rowSize = data.width * 4;
            var tmp;
            if (coords[p1 + 1] > coords[p2 + 1]) {
                tmp = p1;
                p1 = p2;
                p2 = tmp;
                tmp = c1;
                c1 = c2;
                c2 = tmp;
            }
            if (coords[p2 + 1] > coords[p3 + 1]) {
                tmp = p2;
                p2 = p3;
                p3 = tmp;
                tmp = c2;
                c2 = c3;
                c3 = tmp;
            }
            if (coords[p1 + 1] > coords[p2 + 1]) {
                tmp = p1;
                p1 = p2;
                p2 = tmp;
                tmp = c1;
                c1 = c2;
                c2 = tmp;
            }
            var x1 = (coords[p1] + context.offsetX) * context.scaleX;
            var y1 = (coords[p1 + 1] + context.offsetY) * context.scaleY;
            var x2 = (coords[p2] + context.offsetX) * context.scaleX;
            var y2 = (coords[p2 + 1] + context.offsetY) * context.scaleY;
            var x3 = (coords[p3] + context.offsetX) * context.scaleX;
            var y3 = (coords[p3 + 1] + context.offsetY) * context.scaleY;
            if (y1 >= y3) {
                return;
            }
            var c1r = colors[c1], c1g = colors[c1 + 1], c1b = colors[c1 + 2];
            var c2r = colors[c2], c2g = colors[c2 + 1], c2b = colors[c2 + 2];
            var c3r = colors[c3], c3g = colors[c3 + 1], c3b = colors[c3 + 2];

            var minY = Math.round(y1), maxY = Math.round(y3);
            var xa, car, cag, cab;
            var xb, cbr, cbg, cbb;
            var k;
            for (var y = minY; y <= maxY; y++) {
                if (y < y2) {
                    k = y < y1 ? 0 : y1 === y2 ? 1 : (y1 - y) / (y1 - y2);
                    xa = x1 - (x1 - x2) * k;
                    car = c1r - (c1r - c2r) * k;
                    cag = c1g - (c1g - c2g) * k;
                    cab = c1b - (c1b - c2b) * k;
                } else {
                    k = y > y3 ? 1 : y2 === y3 ? 0 : (y2 - y) / (y2 - y3);
                    xa = x2 - (x2 - x3) * k;
                    car = c2r - (c2r - c3r) * k;
                    cag = c2g - (c2g - c3g) * k;
                    cab = c2b - (c2b - c3b) * k;
                }
                k = y < y1 ? 0 : y > y3 ? 1 : (y1 - y) / (y1 - y3);
                xb = x1 - (x1 - x3) * k;
                cbr = c1r - (c1r - c3r) * k;
                cbg = c1g - (c1g - c3g) * k;
                cbb = c1b - (c1b - c3b) * k;
                var x1_ = Math.round(Math.min(xa, xb));
                var x2_ = Math.round(Math.max(xa, xb));
                var j = rowSize * y + x1_ * 4;
                for (var x = x1_; x <= x2_; x++) {
                    k = (xa - x) / (xa - xb);
                    k = k < 0 ? 0 : k > 1 ? 1 : k;
                    bytes[j++] = (car - (car - cbr) * k) | 0;
                    bytes[j++] = (cag - (cag - cbg) * k) | 0;
                    bytes[j++] = (cab - (cab - cbb) * k) | 0;
                    bytes[j++] = 255;
                }
            }
        }

        function drawFigure(data, figure, context) {
            var ps = figure.coords;
            var cs = figure.colors;
            var i, ii;
            switch (figure.type) {
                case 'lattice':
                    var verticesPerRow = figure.verticesPerRow;
                    var rows = Math.floor(ps.length / verticesPerRow) - 1;
                    var cols = verticesPerRow - 1;
                    for (i = 0; i < rows; i++) {
                        var q = i * verticesPerRow;
                        for (var j = 0; j < cols; j++, q++) {
                            drawTriangle(data, context,
                                ps[q], ps[q + 1], ps[q + verticesPerRow],
                                cs[q], cs[q + 1], cs[q + verticesPerRow]);
                            drawTriangle(data, context,
                                ps[q + verticesPerRow + 1], ps[q + 1], ps[q + verticesPerRow],
                                cs[q + verticesPerRow + 1], cs[q + 1], cs[q + verticesPerRow]);
                        }
                    }
                    break;
                case 'triangles':
                    for (i = 0, ii = ps.length; i < ii; i += 3) {
                        drawTriangle(data, context,
                            ps[i], ps[i + 1], ps[i + 2],
                            cs[i], cs[i + 1], cs[i + 2]);
                    }
                    break;
                default:
                    error('illigal figure');
                    break;
            }
        }

        function createMeshCanvas(bounds, combinesScale, coords, colors, figures,
                                  backgroundColor, cachedCanvases) {
            // we will increase scale on some weird factor to let antialiasing take
            // care of "rough" edges
            var EXPECTED_SCALE = 1.1;
            // MAX_PATTERN_SIZE is used to avoid OOM situation.
            var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough
            // We need to keep transparent border around our pattern for fill():
            // createPattern with 'no-repeat' will bleed edges accross entire area.
            var BORDER_SIZE = 2;

            var offsetX = Math.floor(bounds[0]);
            var offsetY = Math.floor(bounds[1]);
            var boundsWidth = Math.ceil(bounds[2]) - offsetX;
            var boundsHeight = Math.ceil(bounds[3]) - offsetY;

            var width = Math.min(Math.ceil(Math.abs(boundsWidth * combinesScale[0] *
                EXPECTED_SCALE)), MAX_PATTERN_SIZE);
            var height = Math.min(Math.ceil(Math.abs(boundsHeight * combinesScale[1] *
                EXPECTED_SCALE)), MAX_PATTERN_SIZE);
            var scaleX = boundsWidth / width;
            var scaleY = boundsHeight / height;

            var context = {
                coords  : coords,
                colors  : colors,
                offsetX : -offsetX,
                offsetY : -offsetY,
                scaleX  : 1 / scaleX,
                scaleY  : 1 / scaleY
            };

            var paddedWidth = width + BORDER_SIZE * 2;
            var paddedHeight = height + BORDER_SIZE * 2;

            var canvas, tmpCanvas, i, ii;
            if (WebGLUtils.isEnabled) {
                canvas = WebGLUtils.drawFigures(width, height, backgroundColor,
                    figures, context);

                // https://bugzilla.mozilla.org/show_bug.cgi?id=972126
                tmpCanvas = cachedCanvases.getCanvas('mesh', paddedWidth, paddedHeight,
                    false);
                tmpCanvas.context.drawImage(canvas, BORDER_SIZE, BORDER_SIZE);
                canvas = tmpCanvas.canvas;
            } else {
                tmpCanvas = cachedCanvases.getCanvas('mesh', paddedWidth, paddedHeight,
                    false);
                var tmpCtx = tmpCanvas.context;

                var data = tmpCtx.createImageData(width, height);
                if (backgroundColor) {
                    var bytes = data.data;
                    for (i = 0, ii = bytes.length; i < ii; i += 4) {
                        bytes[i] = backgroundColor[0];
                        bytes[i + 1] = backgroundColor[1];
                        bytes[i + 2] = backgroundColor[2];
                        bytes[i + 3] = 255;
                    }
                }
                for (i = 0; i < figures.length; i++) {
                    drawFigure(data, figures[i], context);
                }
                tmpCtx.putImageData(data, BORDER_SIZE, BORDER_SIZE);
                canvas = tmpCanvas.canvas;
            }

            return {
                canvas  : canvas,
                offsetX : offsetX - BORDER_SIZE * scaleX,
                offsetY : offsetY - BORDER_SIZE * scaleY,
                scaleX  : scaleX, scaleY : scaleY
            };
        }

        return createMeshCanvas;
    })();

    ShadingIRs.Mesh = {
        fromIR : function Mesh_fromIR(raw) {
            //var type = raw[1];
            var coords = raw[2];
            var colors = raw[3];
            var figures = raw[4];
            var bounds = raw[5];
            var matrix = raw[6];
            //var bbox = raw[7];
            var background = raw[8];
            return {
                type       : 'Pattern',
                getPattern : function Mesh_getPattern(ctx, owner, shadingFill) {
                    var scale;
                    if (shadingFill) {
                        scale = Util.singularValueDecompose2dScale(ctx.mozCurrentTransform);
                    } else {
                        // Obtain scale from matrix and current transformation matrix.
                        scale = Util.singularValueDecompose2dScale(owner.baseTransform);
                        if (matrix) {
                            var matrixScale = Util.singularValueDecompose2dScale(matrix);
                            scale = [scale[0] * matrixScale[0],
                                scale[1] * matrixScale[1]];
                        }
                    }

                    // Rasterizing on the main thread since sending/queue large canvases
                    // might cause OOM.
                    var temporaryPatternCanvas = createMeshCanvas(bounds, scale, coords,
                        colors, figures, shadingFill ? null : background,
                        owner.cachedCanvases);

                    if (!shadingFill) {
                        ctx.setTransform.apply(ctx, owner.baseTransform);
                        if (matrix) {
                            ctx.transform.apply(ctx, matrix);
                        }
                    }

                    ctx.translate(temporaryPatternCanvas.offsetX,
                        temporaryPatternCanvas.offsetY);
                    ctx.scale(temporaryPatternCanvas.scaleX,
                        temporaryPatternCanvas.scaleY);

                    return ctx.createPattern(temporaryPatternCanvas.canvas, 'no-repeat');
                }
            };
        }
    };

    ShadingIRs.Dummy = {
        fromIR : function Dummy_fromIR() {
            return {
                type       : 'Pattern',
                getPattern : function Dummy_fromIR_getPattern() {
                    return 'hotpink';
                }
            };
        }
    };

    function getShadingPatternFromIR(raw) {
        var shadingIR = ShadingIRs[raw[0]];
        if (!shadingIR) {
            error('Unknown IR type: ' + raw[0]);
        }
        return shadingIR.fromIR(raw);
    }

    var TilingPattern = (function TilingPatternClosure() {
        var PaintType = {
            COLORED   : 1,
            UNCOLORED : 2
        };

        var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough

        function TilingPattern(IR, color, ctx, canvasGraphicsFactory, baseTransform) {
            this.operatorList = IR[2];
            this.matrix = IR[3] || [1, 0, 0, 1, 0, 0];
            this.bbox = IR[4];
            this.xstep = IR[5];
            this.ystep = IR[6];
            this.paintType = IR[7];
            this.tilingType = IR[8];
            this.color = color;
            this.canvasGraphicsFactory = canvasGraphicsFactory;
            this.baseTransform = baseTransform;
            this.type = 'Pattern';
            this.ctx = ctx;
        }

        TilingPattern.prototype = {
            createPatternCanvas : function TilinPattern_createPatternCanvas(owner) {
                var operatorList = this.operatorList;
                var bbox = this.bbox;
                var xstep = this.xstep;
                var ystep = this.ystep;
                var paintType = this.paintType;
                var tilingType = this.tilingType;
                var color = this.color;
                var canvasGraphicsFactory = this.canvasGraphicsFactory;

                info('TilingType: ' + tilingType);

                var x0 = bbox[0], y0 = bbox[1], x1 = bbox[2], y1 = bbox[3];

                var topLeft = [x0, y0];
                // we want the canvas to be as large as the step size
                var botRight = [x0 + xstep, y0 + ystep];

                var width = botRight[0] - topLeft[0];
                var height = botRight[1] - topLeft[1];

                // Obtain scale from matrix and current transformation matrix.
                var matrixScale = Util.singularValueDecompose2dScale(this.matrix);
                var curMatrixScale = Util.singularValueDecompose2dScale(
                    this.baseTransform);
                var combinedScale = [matrixScale[0] * curMatrixScale[0],
                    matrixScale[1] * curMatrixScale[1]];

                // MAX_PATTERN_SIZE is used to avoid OOM situation.
                // Use width and height values that are as close as possible to the end
                // result when the pattern is used. Too low value makes the pattern look
                // blurry. Too large value makes it look too crispy.
                width = Math.min(Math.ceil(Math.abs(width * combinedScale[0])),
                    MAX_PATTERN_SIZE);

                height = Math.min(Math.ceil(Math.abs(height * combinedScale[1])),
                    MAX_PATTERN_SIZE);

                var tmpCanvas = owner.cachedCanvases.getCanvas('pattern',
                    width, height, true);
                var tmpCtx = tmpCanvas.context;
                var graphics = canvasGraphicsFactory.createCanvasGraphics(tmpCtx);
                graphics.groupLevel = owner.groupLevel;

                this.setFillAndStrokeStyleToContext(tmpCtx, paintType, color);

                this.setScale(width, height, xstep, ystep);
                this.transformToScale(graphics);

                // transform coordinates to pattern space
                var tmpTranslate = [1, 0, 0, 1, -topLeft[0], -topLeft[1]];
                graphics.transform.apply(graphics, tmpTranslate);

                this.clipBbox(graphics, bbox, x0, y0, x1, y1);

                graphics.executeOperatorList(operatorList);
                return tmpCanvas.canvas;
            },

            setScale : function TilingPattern_setScale(width, height, xstep, ystep) {
                this.scale = [width / xstep, height / ystep];
            },

            transformToScale : function TilingPattern_transformToScale(graphics) {
                var scale = this.scale;
                var tmpScale = [scale[0], 0, 0, scale[1], 0, 0];
                graphics.transform.apply(graphics, tmpScale);
            },

            scaleToContext : function TilingPattern_scaleToContext() {
                var scale = this.scale;
                this.ctx.scale(1 / scale[0], 1 / scale[1]);
            },

            clipBbox : function clipBbox(graphics, bbox, x0, y0, x1, y1) {
                if (bbox && isArray(bbox) && bbox.length === 4) {
                    var bboxWidth = x1 - x0;
                    var bboxHeight = y1 - y0;
                    graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight);
                    graphics.clip();
                    graphics.endPath();
                }
            },

            setFillAndStrokeStyleToContext :
                function setFillAndStrokeStyleToContext(context, paintType, color) {
                    switch (paintType) {
                        case PaintType.COLORED:
                            var ctx = this.ctx;
                            context.fillStyle = ctx.fillStyle;
                            context.strokeStyle = ctx.strokeStyle;
                            break;
                        case PaintType.UNCOLORED:
                            var cssColor = Util.makeCssRgb(color[0], color[1], color[2]);
                            context.fillStyle = cssColor;
                            context.strokeStyle = cssColor;
                            break;
                        default:
                            error('Unsupported paint type: ' + paintType);
                    }
                },

            getPattern : function TilingPattern_getPattern(ctx, owner) {
                var temporaryPatternCanvas = this.createPatternCanvas(owner);

                ctx = this.ctx;
                ctx.setTransform.apply(ctx, this.baseTransform);
                ctx.transform.apply(ctx, this.matrix);
                this.scaleToContext();

                return ctx.createPattern(temporaryPatternCanvas, 'repeat');
            }
        };

        return TilingPattern;
    })();

    exports.getShadingPatternFromIR = getShadingPatternFromIR;
    exports.TilingPattern = TilingPattern;
}));
