/**
 * An array of 2 numbers containing the coordinates of the point: `[x, y]`.
 * @typedef {number[]} Point
 */

/**
 * An array of 4 numbers describing the bounds of the rectangle: `[left, top,
 * right, bottom]`.
 * @typedef {number[]} Rect
 */

(function () {
    /** 
     * @module ai-scripting-utils
     * @typicalname _
     */

    /**
     * Adds one point to another.
     *
     * @memberof module:ai-scripting-utils
     * @category Point Functions
     * @param {Point} point1 The first point.
     * @param {Point} point2 The second point.
     * @returns {Point} The result of adding points.
     */
    function addPoint(point1, point2) {
        point1 = point1 || [];
        point2 = point2 || [0, 0];
        return [+point1[0] + +point2[0], +point1[1] + +point2[1]];
    }

    /**
     * Aligns `item` to `target` on a given side.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {PageItem} item The item to align.
     * @param {(PageItem|Rect)} target The target to align with.
     * @param {("left"|"top"|"right"|"bottom"|"centerX"|"centerY")} side The
     * side to align to.
     * @param {Object} [options]
     * @param {boolean} [options.considerStroke=false] Whether to consider
     * stroke of the items.
     */
    function align(item, target, side, options) {
        if (!isPageItem(item) || !item.editable || !target) return;

        options = options || {};
        var considerStroke = options.considerStroke;

        var boundsType = considerStroke ? "visibleBounds" : "geometricBounds";
        var itemRect = item[boundsType];
        var targetRect = target[boundsType] || target;
        var resultRect = alignRect(itemRect, targetRect, side);
        var dx = resultRect[0] - itemRect[0];
        var dy = resultRect[1] - itemRect[1];

        item.translate(dx, dy);
    }

    /**
     * Aligns one rectangle to another on a given side. Returns the new
     * rectangle.
     *
     * @memberof module:ai-scripting-utils
     * @category Rect Functions
     * @param {Rect} rect The rectangle to align.
     * @param {Rect} target The rectangle to align with.
     * @param {("left"|"top"|"right"|"bottom"|"centerX"|"centerY")} side The
     * side to align to.
     * @returns {Rect} The new aligned rectangle.
     */
    function alignRect(rect, target, side) {
        rect = rect || [];
        target = target || rect;

        var delta = getRectSide(target, side) - getRectSide(rect, side);
        var dx = 0;
        var dy = 0;

        if (side == "left" || side == "right" || side == "centerX") {
            dx = delta;
        } else if (side == "top" || side == "bottom" || side == "centerY") {
            dy = delta;
        }

        return translateRect(rect, dx, dy);
    }

    /**
     * Assigns own enumerable properties of source objects to the destination
     * object. Source objects are applied from left to right. Subsequent sources
     * overwrite property assignments of previous sources. This function mutates
     * `object`.
     *
     * @memberof module:ai-scripting-utils
     * @category Object Functions
     * @param {Object} object The destination object.
     * @param {...Object} [sources] The source objects.
     * @returns {Object} `object`.
     */
    function assign(object, sources) {
        if (object == null || typeof object != "object") object = new Object(object);

        for (var i = 1; i < arguments.length; i++) {
            var source = arguments[i];
            if (source == null) continue;
            for (var key in source) {
                if (source.hasOwnProperty(key)) object[key] = source[key];
            }
        }

        return object;
    }

    function basePad(string, length, chars, padstart, padEnd) {
        string = string == null ? "" : String(string);
        length = length || 0;
        chars = chars == null ? " " : String(chars);

        var padding = "";
        var paddingLength = length - string.length;

        if (chars.length == 0 || paddingLength <= 0) return string;

        for (var i = 0; i < paddingLength; i++) {
            padding += chars[i % chars.length];
        }

        if (padstart && padEnd) {
            var paddingStart = padding.slice(0, Math.floor(paddingLength / 2));
            var paddingEnd = padding.slice(0, Math.ceil(paddingLength / 2));
            return paddingStart + string + paddingEnd;
        } else if (padEnd) {
            return string + padding;
        }

        return padding + string;
    }

    /**
     * Creates a shallow clone of `array`.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to clone.
     * @returns {Array} The cloned array.
     */
    function clone(array) {
        if (isArray(array)) return array.slice();
        if (isArrayLike(array)) return Array.prototype.slice.call(array);
        return [];
    }

    /**
     * Creates a CMYK color. The input values are clamped to the range
     * 0.0-100.0.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {number} [c=0] The cyan component of the color.
     * @param {number} [m=0] The magenta component of the color.
     * @param {number} [y=0] The yellow component of the color.
     * @param {number} [k=0] The black component of the color.
     * @returns {CMYKColor} The new color object.
     */
    function createCMYKColor(c, m, y, k) {
        var colorComps = map([c, m, y, k], function (v) {
            if (isNaN(v) || v < 0) return 0;
            if (v > 100) return 100;
            return +v;
        });
        var color = new CMYKColor();
        color.cyan = colorComps[0];
        color.magenta = colorComps[1];
        color.yellow = colorComps[2];
        color.black = colorComps[3];
        return color;
    }

    /**
     * Creates a group.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} [name] The name of the group.
     * @param {(Document|Layer|GroupItem)} [container=app.activeDocument] The
     * container where the group will be created.
     * @returns {?GroupItem} The new group.
     */
    function createGroup(name, container) {
        if (container == null) container = getActiveDocument();
        if (isDocument(container)) container = container.layers[0];
        if (!isEditable(container)) return;
        var group = container.groupItems.add();
        if (name != null) group.name = name;
        return group;
    }

    /**
     * Creates a layer.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} [name] The name of the layer.
     * @param {(Document|Layer)} [container=app.activeDocument] The container
     * where the layer will be created.
     * @returns {?Layer} The new layer.
     */
    function createLayer(name, container) {
        if (container == null) container = getActiveDocument();
        if (
            (!isDocument(container) && !isLayer(container))
            || (isLayer(container) && !isLayerEditable(container))
        ) {
            return;
        }
        var layer = container.layers.add();
        if (name != null) layer.name = name;
        return layer;
    }

    /**
     * Creates a rectangle.
     *
     * @function module:ai-scripting-utils.createRect
     * @category Rect Functions
     * @param {number} width The width of the rectangle.
     * @param {number} height The height of the rectangle.
     * @param {Point} [fromPoint=[0, 0]] The top left point of the rectangle.
     * @returns {Rect} The new rectangle.
     */
    /**
     * Creates a rectangle.
     *
     * @function module:ai-scripting-utils.createRect
     * @category Rect Functions
     * @param {number} left The left border of the rectangle.
     * @param {number} top The top border of the rectangle.
     * @param {number} right The right border of the rectangle.
     * @param {number} bottom The bottom border of the rectangle.
     * @returns {Rect} The new rectangle.
     */
    /**
     * Creates a rectangle from the passed points. The points do not necessarily
     * need to be the top left and bottom right corners.
     *
     * @function module:ai-scripting-utils.createRect
     * @category Rect Functions
     * @param {Point} fromPoint The first point.
     * @param {Point} toPoint The second point.
     * @returns {Rect} The new rectangle.
     */
    /**
     * Creates a new rectangle from the passed rectangle.
     *
     * @function module:ai-scripting-utils.createRect
     * @category Rect Functions
     * @param {Rect} rect The rectangle to copy.
     * @returns {Rect} The new rectangle.
     */
    function createRect(arg0, arg1, arg2, arg3) {
        var rect = [0, 0, 0, 0];

        if (arguments.length == 1 && isArray(arg0)) {
            rect = [+arg0[0], +arg0[1], +arg0[2], +arg0[3]];
        } else if (arguments.length == 2 && isArray(arg0) && isArray(arg1)) {
            rect = [
                Math.min(arg0[0], arg1[0]),
                Math.max(arg0[1], arg1[1]),
                Math.max(arg0[0], arg1[0]),
                Math.min(arg0[1], arg1[1])
            ];
        } else if (arguments.length < 4 && !isNaN(arg0) && !isNaN(arg1)) {
            if (isArray(arg2)) {
                rect[0] = +arg2[0];
                rect[1] = +arg2[1];
            }
            rect[2] = rect[0] + +arg0;
            rect[3] = rect[1] - arg1;
        } else if (arguments.length >= 4) {
            rect = [+arg0, +arg1, +arg2, +arg3];
        }

        return rect;
    }

    /**
     * Creates a registration color.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {number} [tint=100] The tint of the color.
     * @param {Document} [doc=app.activeDocument] The document whose spots are
     * being searched for the registration spot.
     * @returns {?SpotColor} The new color object.
     */
    function createRegistrationColor(tint, doc) {
        if (doc == null) doc = getActiveDocument();
        if (!isDocument(doc)) return;
        var length = doc.spots.length;
        for (var i = 0; i < length; i++) {
            var spot = doc.spots[i];
            if (isRegistrationSpot(spot)) return createSpotColor(spot, tint);
        }
    }

    /**
     * Creates an RGB color. The input values are clamped to the range 0-255.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {number} [r=0] The red component of the color.
     * @param {number} [g=0] The green component of the color.
     * @param {number} [b=0] The blue component of the color.
     * @returns {RGBColor} The new color object.
     */
    function createRGBColor(r, g, b) {
        var colorComps = map([r, g, b], function (v) {
            if (isNaN(v) || v < 0) return 0;
            if (v > 255) return 255;
            return +v;
        });
        var color = new RGBColor();
        color.red = colorComps[0];
        color.green = colorComps[1];
        color.blue = colorComps[2];
        return color;
    }

    /**
     * Creates a spot.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {Object} [options]
     * @param {*} [options.name] The name of the spot.
     * @param {Color} [options.color] The color of the spot.
     * @param {("spot"|"process")} [options.type="process"] The color model of
     * the spot.
     * @param {Document} [options.doc=app.activeDocument] The document where the
     * spot will be created.
     * @returns {?Spot} The new spot.
     */
    function createSpot(options) {
        options = options || {};
        var doc = options.doc == null ? getActiveDocument() : options.doc;
        if (!isDocument(doc)) return;
        var name = options.name;
        var color = options.color;
        var type = options.type == "spot" ? ColorModel.SPOT : ColorModel.PROCESS;

        var spot = getSpot(name, doc) || doc.spots.add();
        if (name != null) spot.name = name;
        if (isColor(color)) spot.color = color;
        spot.colorType = type;

        return spot;
    }

    /**
     * Creates a spot color. The tint value is clamped to the range 0.0-100.0.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {Spot} spot The spot that defines the color.
     * @param {number} [tint=100] The tint of the color.
     * @returns {SpotColor} The new color object.
     */
    function createSpotColor(spot, tint) {
        if (!isSpot(spot)) return;
        if (isNaN(tint) || tint > 100) {
            tint = 100;
        } else if (tint < 0) {
            tint = 0;
        }
        var spotColor = new SpotColor();
        spotColor.spot = spot;
        spotColor.tint = +tint;
        return spotColor;
    }

    /**
     * Assigns own enumerable properties of source objects to the destination
     * object for all destination properties that resolve to `undefined`. Source
     * objects are applied from left to right. Once a property is set,
     * additional values of the same property are ignored. This function mutates
     * `object`.
     *
     * @memberof module:ai-scripting-utils
     * @category Object Functions
     * @param {Object} object The destination object.
     * @param {...Object} [sources] The source objects.
     * @returns {Object} `object`.
     */
    function defaults(object, sources) {
        if (object == null || typeof object != "object") object = new Object(object);

        for (var i = 1; i < arguments.length; i++) {
            var source = arguments[i];
            if (source == null) continue;
            for (var key in source) {
                if (
                    source.hasOwnProperty(key)
                    && object[key] === undefined
                ) {
                    object[key] = source[key];
                }
            }
        }

        return object;
    }

    /**
     * Draws a circle.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {number} radius The radius of the circle.
     * @param {Point} [center=[0, 0]] The center point.
     * @param {(Document|Layer|GroupItem)} [container=app.activeDocument] The
     * container where the circle will be drawn.
     * @returns {?PathItem} The new circle.
     */
    function drawCircle(radius, center, container) {
        if (container == null) container = getActiveDocument();
        if (isDocument(container)) container = container.layers[0];
        if (
            !container
            || !isEditable(container)
            || isNaN(radius)
        ) {
            return;
        }
        if (!isArray(center) || isNaN(center[0]) || isNaN(center[1])) center = [0, 0];
        var left = center[0] - radius;
        var top = +center[1] + +radius;
        var diameter = radius * 2;
        return container.pathItems.ellipse(top, left, diameter, diameter);
    }

    /**
     * Draws an ellipse.
     *
     * @function module:ai-scripting-utils.drawEllipse
     * @category DOM Functions
     * @param {number} width The width of the ellipse.
     * @param {number} height The height of the ellipse.
     * @param {Point} [fromPoint=[0, 0]] The top left point.
     * @param {(Document|Layer|GroupItem)} [container=app.activeDocument] The
     * container where the ellipse will be drawn.
     * @returns {?PathItem} The new ellipse.
     */
    /**
     * Draws an ellipse from the passed points. The points do not necessarily
     * need to be the top left and bottom right corners.
     *
     * @function module:ai-scripting-utils.drawEllipse
     * @category DOM Functions
     * @param {Point} fromPoint The point to draw from.
     * @param {Point} toPoint The point to draw to.
     * @param {(Document|Layer|GroupItem)} [container=app.activeDocument] The
     * container where the ellipse will be drawn.
     * @returns {?PathItem} The new ellipse.
     */
    /**
     * Draws an ellipse.
     *
     * @function module:ai-scripting-utils.drawEllipse
     * @category DOM Functions
     * @param {Rect} rect The `Rect` object that describes the bounds of the
     * ellipse being drawn.
     * @param {(Document|Layer|GroupItem)} [container=app.activeDocument] The
     * container where the ellipse will be drawn.
     * @returns {?PathItem} The new ellipse.
     */
    function drawEllipse() {
        var args = clone(arguments);
        var container = last(args);
        if (!isDocument(container) && !isLayer(container) && !isGroupItem(container)) {
            container = getActiveDocument();
        } else {
            args.pop();
        }
        if (isDocument(container)) container = container.layers[0];
        if (!isEditable(container)) return;
        var rect = createRect.apply(null, args);
        if (isNaN(rect[0]) || isNaN(rect[1]) || isNaN(rect[2]) || isNaN(rect[3])) return;
        return container.pathItems.ellipse(rect[1], rect[0], rect[2] - rect[0], rect[1] - rect[3]);
    }

    /**
     * Draws a line.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {Point} fromPoint The point to draw from.
     * @param {Point} toPoint The point to draw to.
     * @param {(Document|Layer|GroupItem)} [container=app.activeDocument] The
     * container where the line will be drawn.
     * @returns {?PathItem} The new line.
     */
    function drawLine(fromPoint, toPoint, container) {
        if (container == null) container = getActiveDocument();
        if (isDocument(container)) container = container.layers[0];
        if (
            !container
            || !isEditable(container)
            || !isArray(fromPoint)
            || !isArray(toPoint)
            || isNaN(fromPoint[0])
            || isNaN(fromPoint[1])
            || isNaN(toPoint[0])
            || isNaN(toPoint[1])
        ) {
            return;
        }
        var line = container.pathItems.add();
        // setEntirePath() only accepts arrays with two numeric values in it
        line.setEntirePath([[+fromPoint[0], +fromPoint[1]], [+toPoint[0], +toPoint[1]]]);
        return line;
    }

    /**
     * Draws a rectangle.
     *
     * @function module:ai-scripting-utils.drawRect
     * @category DOM Functions
     * @param {number} width The width of the rectangle.
     * @param {number} height The height of the rectangle.
     * @param {Point} [fromPoint=[0, 0]] The top left point.
     * @param {(Document|Layer|GroupItem)} [container=app.activeDocument] The
     * container where the rectangle will be drawn.
     * @returns {?PathItem} The new rectangle.
     */
    /**
     * Draws a rectangle from the passed points. The points do not necessarily
     * need to be the top left and bottom right corners.
     *
     * @function module:ai-scripting-utils.drawRect
     * @category DOM Functions
     * @param {Point} fromPoint The point to draw from.
     * @param {Point} toPoint The point to draw to.
     * @param {(Document|Layer|GroupItem)} [container=app.activeDocument] The
     * container where the rectangle will be drawn.
     * @returns {?PathItem} The new rectangle.
     */
    /**
     * Draws a rectangle.
     *
     * @function module:ai-scripting-utils.drawRect
     * @category DOM Functions
     * @param {Rect} rect The `Rect` object that describes the bounds of the
     * rectangle being drawn.
     * @param {(Document|Layer|GroupItem)} [container=app.activeDocument] The
     * container where the rectangle will be drawn.
     * @returns {?PathItem} The new rectangle.
     */
    function drawRect() {
        var args = clone(arguments);
        var container = last(args);
        if (!isDocument(container) && !isLayer(container) && !isGroupItem(container)) {
            container = getActiveDocument();
        } else {
            args.pop();
        }
        if (isDocument(container)) container = container.layers[0];
        if (!isEditable(container)) return;
        var rect = createRect.apply(null, args);
        if (isNaN(rect[0]) || isNaN(rect[1]) || isNaN(rect[2]) || isNaN(rect[3])) return;
        return container.pathItems.rectangle(rect[1], rect[0], rect[2] - rect[0], rect[1] - rect[3]);
    }

    /**
     * Executes `callback` for each element of `array`. The callback takes three
     * arguments: `value`, `index`, `array`. The callback may exit iteration
     * early by explicitly returning `false`.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to iterate over.
     * @param {Function} callback The function called for each element.
     * @param {number} [fromIndex=0] The index to iterate from.
     */
    function each(array, callback, fromIndex) {
        if (typeof callback != "function") return;
        if (isNaN(fromIndex) || fromIndex < 0) fromIndex = 0;

        var length = array == null ? 0 : array.length;

        for (var i = fromIndex; i < length; i++) {
            if (callback(array[i], i, array) === false) break;
        }
    }

    /**
     * Traverses a tree of `PageItem` objects starting from a specific root.
     *
     * This function executes `callback` for each child of `root` that is a
     * `PageItem` object. If `root` itself is a `PageItem` object, then
     * `callback` is called for `root` before processing its children. The
     * callback takes one argument: `pageItem`. The callback may exit traversal
     * early by explicitly returning `false`.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {(Document|Layer|PageItem)} root The root of the tree to start
     * traversal from.
     * @param {Function} callback The function called for each `PageItem` object
     * in the tree.
     * @param {Object} [options]
     * @param {boolean} [options.skipHidden=false] Whether to skip hidden items
     * or layers.
     * @param {boolean} [options.skipLocked=false] Whether to skip locked items
     * or layers.
     */
    function eachPageItem(root, callback, options) {
        if (typeof callback != "function") return;
        options = options || {};

        var skipHidden = options.skipHidden;
        var skipLocked = options.skipLocked;

        (function diveIn(root) {
            if (isPageItem(root)) {
                if (skipHidden && root.hidden) return;
                if (skipLocked && root.locked) return;
                if (callback(root) === false) return false;
                if (isGroupItem(root)) diveIn(root.pageItems);
            } else if (isDOMCollection(root)) {
                for (var i = 0, j = root.length; i < j; i++) {
                    if (diveIn(root[i]) === false) return false;
                }
            } else if (isLayer(root)) {
                if (skipHidden && !root.visible) return;
                if (skipLocked && root.locked) return;
                if (diveIn(root.pageItems) === false) return;
                diveIn(root.layers);
            } else if (isDocument(root)) {
                diveIn(root.layers);
            }
        })(root);
    }

    /**
     * Checks if `string` ends with `target` string.
     *
     * @memberof module:ai-scripting-utils
     * @category String Functions
     * @param {string} string The string to inspect.
     * @param {string} target The string to search for.
     * @returns {boolean} `true` if `string` ends with `target`, `false`
     * otherwise.
     */
    function endsWith(string, target) {
        string = string == null ? "" : String(string);
        var targetLength = target == null ? 0 : target.length;
        return string.indexOf(target) + targetLength == string.length;
    }

    /**
     * Returns an array of own enumerable property key-value pairs for `object`.
     *
     * @memberof module:ai-scripting-utils
     * @category Object Functions
     * @param {Object} object The object to query.
     * @returns {Array} The array of key-value pairs.
     */
    function entries(object) {
        if (object == null || typeof object != "object") return [];

        var result = [];

        for (var key in object) {
            if (object.hasOwnProperty(key)) result.push([key, object[key]]);
        }

        return result;
    }

    /**
     * Checks if `callback` returns a truly value for all elements of `array`.
     * The callback takes three arguments: `value`, `index`, `array`.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to iterate over.
     * @param {Function} callback The function called for each element.
     * @returns {boolean} `true` if all elements pass the check, `false`
     * otherwise.
     */
    function every(array, callback) {
        var length = array == null ? 0 : array.length;

        if (typeof callback != "function") return true;

        for (var i = 0; i < length; i++) {
            if (!callback(array[i], i, array)) return false;
        }

        return true;
    }

    /**
     * Expands `rect` by the given amount. Returns the new rectangle.
     *
     * @function module:ai-scripting-utils.expandRect
     * @category Rect Functions
     * @param {Rect} rect The rectangle to expand.
     * @param {number} amount The amount to expand by.
     * @returns {Rect} The new expanded rectangle.
     */
    /**
     * Expands `rect` by the given horizontal and vertical amounts. Returns the
     * new rectangle.
     *
     * @function module:ai-scripting-utils.expandRect
     * @category Rect Functions
     * @param {Rect} rect The rectangle to expand.
     * @param {number} hor The amount to expand horizontally.
     * @param {number} ver The amount to expand vertically.
     * @returns {Rect} The new expanded rectangle.
     */
    function expandRect(rect, arg1, arg2) {
        var hor = arg1 || 0;
        var ver = arg2 || hor;

        return offsetRect(rect, {
            left: hor / 2,
            top: ver / 2,
            right: hor / 2,
            bottom: ver / 2
        });
    }

    /**
     * Fills `array` with `value`. This function mutates `array`.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to fill.
     * @param {*} value The filling value.
     * @returns {Array} `array`.
     */
    function fill(array, value) {
        var length = array == null ? 0 : array.length;

        if (!length) return [];

        for (var i = 0; i < length; i++) {
            array[i] = value;
        }

        return array;
    }

    /**
     * Creates a new array of those elements for which `callback` returned a
     * truthy value. The callback takes three arguments: `value`, `index`,
     * `array`.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to iterate over.
     * @param {Function} callback The function called for each element.
     * @returns {Array} The new filtered array.
     */
    function filter(array, callback) {
        if (typeof callback != "function") return clone(array);

        var length = array == null ? 0 : array.length;
        var result = [];
        var resIndex = 0;

        for (var i = 0; i < length; i++) {
            var value = array[i];
            if (callback(value, i, array)) result[resIndex++] = value;
        }

        return result;
    }

    /**
     * Returns the first value in `array` for which `callback` returned a truly
     * value. The callback takes three arguments: `value`, `index`, `array`.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to inspect.
     * @param {Function} callback The function called for each element.
     * @param {number} [fromIndex=0] The index to search from.
     * @returns {*} The matched element, `undefined` otherwise.
     */
    function find(array, callback, fromIndex) {
        var i = findIndex(array, callback, fromIndex);
        if (i > -1) return array[i];
    }

    /**
     * Returns the index of the first value in `array` for which `callback`
     * returned a truly value. The callback takes three arguments: `value`,
     * `index`, `array`.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to inspect.
     * @param {Function} callback The function called for each element.
     * @param {number} [fromIndex=0] The index to search from.
     * @returns {number} The index of the found element, -1 otherwise.
     */
    function findIndex(array, callback, fromIndex) {
        if (typeof callback != "function") return -1;
        if (isNaN(fromIndex) || fromIndex < 0) fromIndex = 0;

        var length = array == null ? 0 : array.length;

        for (var i = fromIndex; i < length; i++) {
            if (callback(array[i], i, array)) return i;
        }

        return -1;
    }

    /**
     * Gets the active document.
     *
     * If there are no documents open, accessing `app.activeDocument` will
     * result in an error. In such a case this function just returns `null`,
     * making getting the active document more convenient.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @returns {?Document} `app.activeDocument` or `null` if there are no
     * documents open.
     */
    function getActiveDocument() {
        return app.documents.length > 0 ? app.activeDocument : null;
    }

    /**
     * Returns the distance between two points.
     *
     * @memberof module:ai-scripting-utils
     * @category Point Functions
     * @param {Point} point1 The first point.
     * @param {Point} point2 The second point.
     * @returns {number} The distance between points.
     */
    function getDistance(point1, point2) {
        point1 = point1 || [];
        point2 = point2 || point1;
        return Math.sqrt(
            Math.pow(point2[0] - point1[0], 2)
            + Math.pow(point2[1] - point1[1], 2)
        );
    }

    /**
     * Returns the inclination angle of the line drawn through two points. The
     * angle is measured counterclockwise and is in the range 0.0-180.0.
     *
     * @memberof module:ai-scripting-utils
     * @category Point Functions
     * @param {Point} point1 The first point.
     * @param {Point} point2 The second point.
     * @returns {number} The inclination angle.
     */
    function getInclination(point1, point2) {
        point1 = point1 || [];
        point2 = point2 || point1;
        var leg1 = point2[1] - point1[1];
        var leg2 = point2[0] - point1[0];
        var angle = Math.atan(leg1 / leg2) * 180 / Math.PI;
        return angle >= 0 ? angle : angle + 180;
    }

    /**
     * Gets the layer by its name among the container's child layers.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} name The name of the layer to find.
     * @param {(Document|Layer)} [container=app.activeDocument] The container
     * whose children are being searched for.
     * @returns {?Layer} The found layer or `null` if the layer is not found.
     */
    function getLayer(name, container) {
        if (container == null) container = getActiveDocument();
        try {
            // If the layer doesn't exist, an exception will be thrown in this
            // block
            return container.layers.getByName(name);
        } catch (e) {
            return null;
        }
    }

    /**
     * Returns the area of the rectangle.
     *
     * @memberof module:ai-scripting-utils
     * @category Rect Functions
     * @param {Rect} rect The rectangle to query.
     * @returns {number} The area of `rect`.
     */
    function getRectArea(rect) {
        return getRectWidth(rect) * getRectHeight(rect);
    }

    /**
     * Returns the height of the rectangle.
     *
     * @memberof module:ai-scripting-utils
     * @category Rect Functions
     * @param {Rect} rect The rectangle to query.
     * @returns {number} The height of `rect`.
     */
    function getRectHeight(rect) {
        rect = rect || [];
        return rect[1] - rect[3];
    }

    /**
     * Returns the point of `rect` from the specified location.
     *
     * @memberof module:ai-scripting-utils
     * @category Rect Functions
     * @param {Rect} rect The rectangle to query.
     * @param {("topLeft"|"topRight"|"bottomLeft"|"bottomRight"|"center")}
     * location The location of the point.
     * @returns {?Point} The point of `rect`.
     */
    function getRectPoint(rect, location) {
        rect = rect || [];

        switch (location) {
            case "topLeft":
                return [+rect[0], +rect[1]];
            case "topRight":
                return [+rect[2], +rect[1]];
            case "bottomLeft":
                return [+rect[0], +rect[3]];
            case "bottomRight":
                return [+rect[2], +rect[3]];
            case "center":
                return [getRectSide(rect, "centerX"), getRectSide(rect, "centerY")];
        }
    }

    /**
     * Returns the position of the desired side of `rect`.
     *
     * @memberof module:ai-scripting-utils
     * @category Rect Functions
     * @param {Rect} rect The rectangle to query.
     * @param {("left"|"top"|"right"|"bottom"|"centerX"|"centerY")} side The
     * desired side.
     * @returns {?number} The position of the side of `rect`.
     */
    function getRectSide(rect, side) {
        rect = rect || [];

        switch (side) {
            case "left":
                return +rect[0];
            case "top":
                return +rect[1];
            case "right":
                return +rect[2];
            case "bottom":
                return +rect[3];
            case "centerX":
                return +rect[0] + (rect[2] - rect[0]) / 2;
            case "centerY":
                return +rect[1] + (rect[3] - rect[1]) / 2;
        }
    }

    /**
     * Returns the width of the rectangle.
     *
     * @memberof module:ai-scripting-utils
     * @category Rect Functions
     * @param {Rect} rect The rectangle to query.
     * @returns {number} The width of `rect`.
     */
    function getRectWidth(rect) {
        rect = rect || [];
        return rect[2] - rect[0];
    }

    /**
     * Gets the spot by its name.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} name The name of the spot to find.
     * @param {Document} [doc=app.activeDocument] The document whose spots are
     * being searched for.
     * @returns {?Spot} The found spot or `null` if the spot is not found.
     */
    function getSpot(name, doc) {
        if (doc == null) doc = getActiveDocument();
        try {
            // If the spot doesn't exist, an exception will be thrown in this
            // block
            return doc.spots.getByName(name);
        } catch (e) {
            return null;
        }
    }

    /**
     * Returns the smallest rectangle containing `rect1` and `rect2`.
     *
     * @memberof module:ai-scripting-utils
     * @category Rect Functions
     * @param {Rect} rect1 The first rectangle.
     * @param {Rect} rect2 The second rectangle.
     * @returns {Rect} The containing rectangle.
     */
    function includeRects(rect1, rect2) {
        rect1 = rect1 || rect2 || [];
        rect2 = rect2 || rect1 || [];
        return [
            Math.min(rect1[0], rect2[0]),
            Math.max(rect1[1], rect2[1]),
            Math.max(rect1[2], rect2[2]),
            Math.min(rect1[3], rect2[3])
        ];
    }

    /**
     * Checks if `collection` includes `value`.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {(Array|Object|string)} collection The collection to inspect.
     * @param {*} value The value to search for.
     * @param {number} [fromIndex=0] The index to search from.
     * @returns {boolean} `true` if `value` is found, `false` otherwise.
     */
    function includes(collection, value, fromIndex) {
        if (typeof collection == "string") {
            return collection.indexOf(value, fromIndex) > -1;
        }

        if (isArrayLike(collection)) {
            return indexOf(collection, value, fromIndex) > -1;
        }

        if (typeof collection == "object" && collection !== null) {
            for (var key in collection) {
                if (
                    collection.hasOwnProperty(key)
                    && collection[key] === value
                ) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Returns the index of the first occurance of `value` in `array`.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to inspect.
     * @param {*} value The value to search for.
     * @param {number} [fromIndex=0] The index to search from.
     * @returns {number} The index of the found element, -1 otherwise.
     */
    function indexOf(array, value, fromIndex) {
        if (isNaN(fromIndex) || fromIndex < 0) fromIndex = 0;

        var length = array == null ? 0 : array.length;

        for (var i = fromIndex; i < length; i++) {
            if (array[i] === value) return i;
        }

        return -1;
    }

    /**
     * Checks if `number` is in the range `[0,1)`.
     *
     * @function module:ai-scripting-utils.inRange
     * @category Utility Functions
     * @param {number} number The number to check.
     * @returns {boolean} `true` if `number` is in the range, `false` otherwise.
     */
    /**
     * Checks if `number` is in the range `[0,upper)`. If `upper` is less than
     * `0` the range boundaries are swapped.
     *
     * @function module:ai-scripting-utils.inRange
     * @category Utility Functions
     * @param {number} number The number to check.
     * @param {number} upper The upper bound of the range.
     * @returns {boolean} `true` if `number` is in the range, `false` otherwise.
     */
    /**
     * Checks if `number` is in the range `[lower,upper)`. If `lower` is greater
     * than `upper` the range boundaries are swapped.
     *
     * @function module:ai-scripting-utils.inRange
     * @category Utility Functions
     * @param {number} number The number to check.
     * @param {number} lower The lower bound of the range.
     * @param {number} upper The upper bound of the range.
     * @returns {boolean} `true` if `number` is in the range, `false` otherwise.
     */
    function inRange(number, arg1, arg2) {
        var lower = 0;
        var upper = 1;

        if (arguments.length == 2) {
            upper = arg1;
        } else if (arguments.length >= 3) {
            lower = arg1;
            upper = arg2;
        }

        if (lower > upper) {
            var tmp = lower;
            lower = upper;
            upper = tmp;
        }

        return number >= lower && number < upper;
    }

    /**
     * Returns the result of the intersection of two rectangles.
     *
     * @memberof module:ai-scripting-utils
     * @category Rect Functions
     * @param {Rect} rect1 The first rectangle.
     * @param {Rect} rect2 The second rectangle.
     * @returns {?Rect} The intersection rectangle or `null` if the rectangles
     * don't intersect.
     */
    function intersectRects(rect1, rect2) {
        rect1 = rect1 || [];
        rect2 = rect2 || [];
        if (!isRectsIntersect(rect1, rect2)) return null;
        return [
            Math.max(rect1[0], rect2[0]),
            Math.min(rect1[1], rect2[1]),
            Math.min(rect1[2], rect2[2]),
            Math.max(rect1[3], rect2[3])
        ];
    }

    /**
     * Checks if `value` is an `Array` object.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is an array, `false` otherwise.
     */
    function isArray(value) {
        return Object.prototype.toString.call(value) == "[object Array]";
    }

    /**
     * Checks if `value` is array-like.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is array-like, `false` otherwise.
     */
    function isArrayLike(value) {
        if (value != null && typeof value != "function") {
            var length = value.length;
            return typeof length == "number" && length >= 0 && length % 1 == 0;
        }
        return false;
    }

    /**
     * Checks if `value` is a `CMYKColor` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `CMYKColor` object, `false`
     * otherwise.
     */
    function isCMYKColor(value) {
        return !!value && value.typename == "CMYKColor";
    }

    /**
     * Checks if `value` is a `Color` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `Color` object, `false`
     * otherwise.
     */
    function isColor(value) {
        return isCMYKColor(value)
            || isRGBColor(value)
            || isSpotColor(value)
            || isGrayColor(value)
            || isGradientColor(value)
            || isPatternColor(value)
            || isLabColor(value)
            || isNoColor(value);
    }

    /**
     * Checks if `value` is a `CompoundPathItem` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `CompoundPathItem` object,
     * `false` otherwise.
     */
    function isCompoundPathItem(value) {
        return !!value && value.typename == "CompoundPathItem";
    }

    /**
     * Checks if `value` is a `Document` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `Document` object, `false`
     * otherwise.
     */
    function isDocument(value) {
        return !!value && value.typename == "Document";
    }

    /**
     * Checks if `value` is a native collection of DOM objects. These objects
     * are not arrays but they contain children that can be accessed by index
     * using bracket notation. Examples of classes that are DOM object
     * collections: `Documents`, `Layers`, `PageItems`, `Spots`, `Characters`,
     * etc.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a DOM object collection, `false`
     * otherwise.
     */
    function isDOMCollection(value) {
        return !!value && typeof value[".index"] == "function";
    }

    /**
     * Checks if `value` is an editable `Layer` or `PageItem` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {(Layer|PageItem)} value The value to check.
     * @returns {boolean} `true` if `value` is editable, `false` otherwise.
     */
    function isEditable(value) {
        if (isPageItem(value)) return value.editable;
        if (isLayer(value)) return isLayerEditable(value);
        return false;
    }

    /**
     * Checks if `value` is a `GradientColor` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `GradientColor` object, `false`
     * otherwise.
     */
    function isGradientColor(value) {
        return !!value && value.typename == "GradientColor";
    }

    /**
     * Checks if `value` is a `GraphItem` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `GraphItem` object, `false`
     * otherwise.
     */
    function isGraphItem(value) {
        return !!value && value.typename == "GraphItem";
    }

    /**
     * Checks if `value` is a `GrayColor` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `GrayColor` object, `false`
     * otherwise.
     */
    function isGrayColor(value) {
        return !!value && value.typename == "GrayColor";
    }

    /**
     * Checks if `value` is a `GroupItem` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `GroupItem` object, `false`
     * otherwise.
     */
    function isGroupItem(value) {
        return !!value && value.typename == "GroupItem";
    }

    /**
     * Checks if `value` is a `LabColor` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `LabColor` object, `false`
     * otherwise.
     */
    function isLabColor(value) {
        return !!value && value.typename == "LabColor";
    }

    /**
     * Checks if `value` is a `Layer` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `Layer` object, `false`
     * otherwise.
     */
    function isLayer(value) {
        return !!value && value.typename == "Layer";
    }

    /**
     * Checks if `layer` and all of its ancestors are editable. If the passed
     * layer is nor hidden or locked but one of its ancestors are, the return
     * value will be `false`.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {Layer} layer The layer to check.
     * @returns {boolean} `true` if `layer` and all of its ancestors are
     * editable, `false` otherwise.
     */
    function isLayerEditable(layer) {
        if (!isLayer(layer)) return false;
        do {
            if (!layer.visible || layer.locked) return false;
            layer = layer.parent;
        } while (!isDocument(layer));
        return true;
    }

    /**
     * Checks if `value` is a `LegacyTextItem` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `LegacyTextItem` object,
     * `false` otherwise.
     */
    function isLegacyTextItem(value) {
        return !!value && value.typename == "LegacyTextItem";
    }

    /**
     * Checks if `value` is a `MeshItem` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `MeshItem` object, `false`
     * otherwise.
     */
    function isMeshItem(value) {
        return !!value && value.typename == "MeshItem";
    }

    /**
     * Checks if `value` is a `NoColor` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `NoColor` object, `false`
     * otherwise.
     */
    function isNoColor(value) {
        return !!value && value.typename == "NoColor";
    }

    /**
     * Checks if `value` is a `NonNativeItem` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `NonNativeItem` object, `false`
     * otherwise.
     */
    function isNonNativeItem(value) {
        return !!value && value.typename == "NonNativeItem";
    }

    /**
     * Checks if `value` is a `PageItem` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `PageItem` object, `false`
     * otherwise.
     */
    function isPageItem(value) {
        return isPathItem(value)
            || isGroupItem(value)
            || isCompoundPathItem(value)
            || isTextFrame(value)
            || isPlacedItem(value)
            || isRasterItem(value)
            || isGraphItem(value)
            || isMeshItem(value)
            || isPluginItem(value)
            || isLegacyTextItem(value)
            || isNonNativeItem(value);
    }

    /**
     * Checks if `value` is a `PathItem` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `PathItem` object, `false`
     * otherwise.
     */
    function isPathItem(value) {
        return !!value && value.typename == "PathItem";
    }

    /**
     * Checks if `value` is a `PatternColor` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `PatternColor` object, `false`
     * otherwise.
     */
    function isPatternColor(value) {
        return !!value && value.typename == "PatternColor";
    }

    /**
     * Checks if `value` is a `PlacedItem` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `PlacedItem` object, `false`
     * otherwise.
     */
    function isPlacedItem(value) {
        return !!value && value.typename == "PlacedItem";
    }

    /**
     * Checks if `value` is a `PluginItem` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `PluginItem` object, `false`
     * otherwise.
     */
    function isPluginItem(value) {
        return !!value && value.typename == "PluginItem";
    }

    /**
     * Checks if `value` is a `RasterItem` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `RasterItem` object, `false`
     * otherwise.
     */
    function isRasterItem(value) {
        return !!value && value.typename == "RasterItem";
    }

    /**
     * Checks if two rectangles intersect.
     *
     * @memberof module:ai-scripting-utils
     * @category Rect Functions
     * @param {Rect} rect1 The first rectangle.
     * @param {Rect} rect2 The second rectangle.
     * @returns {boolean} `true` if the rectangles intersect, `false` otherwise.
     */
    function isRectsIntersect(rect1, rect2) {
        rect1 = rect1 || [];
        rect2 = rect2 || [];
        return rect1[0] < rect2[2]
            && rect1[1] > rect2[3]
            && rect1[2] > rect2[0]
            && rect1[3] < rect2[1];
    }

    /**
     * Checks if `value` is a registration color.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a registration color, `false`
     * otherwise.
     */
    function isRegistrationColor(value) {
        return isSpotColor(value) && isRegistrationSpot(value.spot);
    }

    /**
     * Checks if `value` is a registration spot.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a registration spot, `false`
     * otherwise.
     */
    function isRegistrationSpot(value) {
        return isSpot(value)
            && value.colorType == ColorModel.REGISTRATION;
    }

    /**
     * Checks if `value` is a `RGBColor` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `RGBColor` object, `false`
     * otherwise.
     */
    function isRGBColor(value) {
        return !!value && value.typename == "RGBColor";
    }

    /**
     * Checks if `value` is a `Spot` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `Spot` object, `false`
     * otherwise.
     */
    function isSpot(value) {
        return !!value && value.typename == "Spot";
    }

    /**
     * Checks if `value` is a `SpotColor` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `SpotColor` object, `false`
     * otherwise.
     */
    function isSpotColor(value) {
        return !!value && value.typename == "SpotColor";
    }

    /**
     * Checks if `value` is a `TextFrame` object.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {*} value The value to check.
     * @returns {boolean} `true` if `value` is a `TextFrame` object, `false`
     * otherwise.
     */
    function isTextFrame(value) {
        return !!value && value.typename == "TextFrame";
    }

    /**
     * Returns an array of own enumerable property keys for `object`.
     *
     * @memberof module:ai-scripting-utils
     * @category Object Functions
     * @param {Object} object The object to query.
     * @returns {Array} The array of property keys.
     */
    function keys(object) {
        if (object == null || typeof object != "object") return [];

        var result = [];

        for (var key in object) {
            if (object.hasOwnProperty(key)) result.push(key);
        }

        return result;
    }

    /**
     * Retrieves the last element of `array`.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to query.
     * @returns {*} The last element of `array`.
     */
    function last(array) {
        if (array != null) return array[array.length - 1];
    }

    /**
     * This function is like `indexOf` except that it iterates over elements of
     * `array` from right to left.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to inspect.
     * @param {*} value The value to search for.
     * @param {number} [fromIndex=array.length-1] The index to search from.
     * @returns {number} The index of the found element, -1 otherwise.
     */
    function lastIndexOf(array, value, fromIndex) {
        var length = array == null ? 0 : array.length;

        if (isNaN(fromIndex) || fromIndex >= length) fromIndex = length - 1;

        for (var i = fromIndex; i >= 0; i--) {
            if (array[i] === value) return i;
        }

        return -1;
    }

    /**
     * Prints `message` to the console. It is a shorthand for `$.writeln()`.
     *
     * @memberof module:ai-scripting-utils
     * @category Utility Functions
     * @param {*} message The message to print.
     */
    function log(message) {
        $.writeln(message);
    }

    /**
     * Creates a new array populated with the results of `callback` invocation
     * for each element of `array`. The callback takes three arguments: `value`,
     * `index`, `array`.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to iterate over.
     * @param {Function} callback The function called for each element.
     * @returns {Array} The new mapped array.
     */
    function map(array, callback) {
        if (typeof callback != "function") return clone(array);

        var length = array == null ? 0 : array.length;
        var result = new Array(length);

        for (var i = 0; i < length; i++) {
            result[i] = callback(array[i], i, array);
        }

        return result;
    }

    /**
     * Moves `item` to the specified point.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {PageItem} item The item to move.
     * @param {Point} toPoint The point to move to.
     * @param {Object} [options]
     * @param {Point} [options.fromPoint] The point to move from. Defaults to
     * the top left corner of `item`.
     * @param {boolean} [options.considerStroke=false] Whether to consider
     * stroke of the item.
     */
    function move(item, toPoint, options) {
        if (!isPageItem(item) || !item.editable || !toPoint) return;

        options = options || {};
        var considerStroke = options.considerStroke;

        var boundsType = considerStroke ? "visibleBounds" : "geometricBounds";
        var itemRect = item[boundsType];
        var fromPoint = options.fromPoint || [itemRect[0], itemRect[1]];
        var delta = subtractPoint(toPoint, fromPoint);

        item.translate(delta[0] || 0, delta[1] || 0);
    }

    /**
     * Moves `rect` to the specified point. Returns the new rectangle.
     *
     * @memberof module:ai-scripting-utils
     * @category Rect Functions
     * @param {Rect} rect The rectangle to move.
     * @param {Point} toPoint The point to move to.
     * @param {Point} [fromPoint=[rect[0], rect[1]]] The point to move from.
     * Defaults to the top left corner of `rect`.
     * @returns {Rect} The new moved rectangle.
     */
    function moveRect(rect, toPoint, fromPoint) {
        rect = rect || [];
        fromPoint = fromPoint || [rect[0], rect[1]];
        toPoint = toPoint || fromPoint;

        var delta = subtractPoint(toPoint, fromPoint);

        return translateRect(rect, delta[0], delta[1]);
    }

    /**
     * Places `item` from `target` with an offset.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {PageItem} item The item to place.
     * @param {(PageItem|Rect)} target The target to offset from.
     * @param {("left"|"top"|"right"|"bottom")} side The side of `target` to
     * offset from.
     * @param {Object} [options]
     * @param {boolean} [options.amount=0] The offset amount.
     * @param {boolean} [options.considerStroke=false] Whether to consider
     * stroke of the items.
     */
    function offsetFrom(item, target, side, options) {
        if (!isPageItem(item) || !item.editable || !target) return;

        options = options || {};
        var amount = options.amount;
        var considerStroke = options.considerStroke;

        var boundsType = considerStroke ? "visibleBounds" : "geometricBounds";
        var itemRect = item[boundsType];
        var targetRect = target[boundsType] || target;
        var resultRect = offsetRectFrom(itemRect, targetRect, side, amount);
        var dx = resultRect[0] - itemRect[0];
        var dy = resultRect[1] - itemRect[1];

        item.translate(dx, dy);
    }

    /**
     * Offsets `rect` by the given amount. Returns the new rectangle.
     *
     * @function module:ai-scripting-utils.offsetRect
     * @category Rect Functions
     * @param {Rect} rect The rectangle to offset.
     * @param {number} [amount=0] The amount to offset by.
     * @returns {Rect} The new offsetted rectangle.
     */
    /**
     * Offsets `rect` by the given amounts. Returns the new rectangle.
     *
     * @function module:ai-scripting-utils.offsetRect
     * @category Rect Functions
     * @param {Rect} rect The rectangle to offset.
     * @param {Object} [object]
     * @param {number} [object.left=0] The amount to offset from the left.
     * @param {number} [object.top=0] The amount to offset from the top.
     * @param {number} [object.right=0] The amount to offset from the right.
     * @param {number} [object.bottom=0] The amount to offset from the bottom.
     * @returns {Rect} The new offsetted rectangle.
     */
    function offsetRect(rect, arg1) {
        rect = rect || [];
        arg1 = arg1 || 0;

        var dleft, dright, dtop, dbottom;

        if (typeof arg1 == "object") {
            dleft = arg1.left || 0;
            dright = arg1.right || 0;
            dtop = arg1.top || 0;
            dbottom = arg1.bottom || 0;
        } else {
            dleft = dright = dtop = dbottom = arg1;
        }

        var result = [
            rect[0] - dleft,
            +rect[1] + +dtop,
            +rect[2] + +dright,
            rect[3] - dbottom
        ];

        if (getRectWidth(result) < 0) {
            result[0] = result[2] = getRectSide(result, "centerX");
        }
        if (getRectHeight(result) < 0) {
            result[1] = result[3] = getRectSide(result, "centerY");
        }

        return result;
    }

    /**
     * Places one rectangle from the other with an offset. Returns the new
     * rectangle.
     *
     * @memberof module:ai-scripting-utils
     * @category Rect Functions
     * @param {Rect} rect The rectangle to place.
     * @param {Rect} target The rectangle to offset from.
     * @param {("left"|"top"|"right"|"bottom")} side The side of `target` to
     * offset from.
     * @param {number} [amount=0] The offset amount.
     * @returns {Rect} The new placed rectangle.
     */
    function offsetRectFrom(rect, target, side, amount) {
        rect = rect || [];
        target = target || [0, 0, 0, 0];
        amount = amount || 0;

        var dx = 0;
        var dy = 0;

        switch (side) {
            case "left":
                dx = target[0] - rect[2] - amount;
                break;
            case "top":
                dy = target[1] - rect[3] + amount;
                break;
            case "right":
                dx = target[2] - rect[0] + amount;
                break;
            case "bottom":
                dy = target[3] - rect[1] - amount;
                break;
        }

        return translateRect(rect, dx, dy);
    }

    /**
     * Evenly pads the beginning and end of `string` with `chars` until the
     * resulting string reaches `length`.
     *
     * @memberof module:ai-scripting-utils
     * @category String Functions
     * @param {string} string The string to pad.
     * @param {number} [length=0] The padding length.
     * @param {string} [chars=​ ​] The padding string.
     * @returns {string} The padded string.
     */
    function pad(string, length, chars) {
        return basePad(string, length, chars, true, true);
    }

    /**
     * Pads the end of `string` with `chars` until the resulting string reaches
     * `length`.
     *
     * @memberof module:ai-scripting-utils
     * @category String Functions
     * @param {string} string The string to pad.
     * @param {number} [length=0] The padding length.
     * @param {string} [chars=​ ​] The padding string.
     * @returns {string} The padded string.
     */
    function padEnd(string, length, chars) {
        return basePad(string, length, chars, false, true);
    }

    /**
     * Pads the beginning of `string` with `chars` until the resulting string
     * reaches `length`.
     *
     * @memberof module:ai-scripting-utils
     * @category String Functions
     * @param {string} string The string to pad.
     * @param {number} [length=0] The padding length.
     * @param {string} [chars=​ ​] The padding string.
     * @returns {string} The padded string.
     */
    function padStart(string, length, chars) {
        return basePad(string, length, chars, true);
    }

    /**
     * Returns a random number between the inclusive `0` and `1` bounds. If
     * `floating` is `true` a floating point number is returned.
     *
     * @function module:ai-scripting-utils.random
     * @category Utility Functions
     * @param {boolean} [floating=false] Whether a floating point number should
     * be returned.
     * @returns {number} The random number.
     */
    /**
     * Returns a random number between the inclusive `0` and `upper` bounds. If
     * either `floating` is `true` or `upper` is a float, a floating point
     * number is returned. If `upper` is less than `0` the range boundaries are
     * swapped.
     *
     * @function module:ai-scripting-utils.random
     * @category Utility Functions
     * @param {number} upper The upper bound.
     * @param {boolean} [floating=false] Whether a floating point number should
     * be returned.
     * @returns {number} The random number.
     */
    /**
     * Returns a random number between the inclusive `lower` and `upper` bounds.
     * If `floating` is `true`, or either `lower` or `upper` are floats, a
     * floating point number is returned. If `lower` is greater than `upper` the
     * range boundaries are swapped.
     *
     * @function module:ai-scripting-utils.random
     * @category Utility Functions
     * @param {number} lower The lower bound.
     * @param {number} upper The upper bound.
     * @param {boolean} [floating=false] Whether a floating point number should
     * be returned.
     * @returns {number} The random number.
     */
    function random(arg0, arg1, arg2) {
        var lower = 0;
        var upper = 1;
        var floating = false;

        if (arguments.length == 1) {
            if (typeof arg0 == "boolean") {
                floating = arg0;
            } else {
                upper = arg0;
            }
        } else if (arguments.length == 2) {
            if (typeof arg1 == "boolean") {
                upper = arg0;
                floating = arg1;
            } else {
                lower = arg0;
                upper = arg1;
            }
        } else if (arguments.length >= 3) {
            lower = arg0;
            upper = arg1;
            floating = arg2;
        }

        lower = Number(lower);
        upper = Number(upper);
        floating = Boolean(floating);

        if (isNaN(lower) || isNaN(upper)) return 0;

        if (lower % 1 != 0 || upper % 1 != 0) floating = true;
        if (lower > upper) {
            var tmp = lower;
            lower = upper;
            upper = tmp;
        }
        if (!floating) upper += 1;

        var result = lower + (upper - lower) * Math.random();
        return floating ? result : Math.floor(result);
    }

    /**
     * Executes `callback` for each element of `array`, resulting in a single
     * output value. If `accumulator` is not given, the first element of `array`
     * is used as the initial value. The callback takes four arguments:
     * `accumulator`, `value`, `index`, `array`.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to iterate over.
     * @param {Function} callback The function called for each element.
     * @param {*} [accumulator] The initial value.
     * @returns {*} The accumulated value.
     */
    function reduce(array, callback, accumulator) {
        var length = array == null ? 0 : array.length;
        var i = 0;

        if (length) {
            if (typeof callback != "function") return array[i];
            if (arguments.length < 3) accumulator = array[i++];
        }

        for (; i < length; i++) {
            accumulator = callback(accumulator, array[i], i, array);
        }

        return accumulator;
    }

    /**
     * This function is like `reduce` except that it iterates over elements of
     * `array` from right to left, and if `accumulator` is not given, the last
     * element of `array` is used as the initial value.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to iterate over.
     * @param {Function} callback The function called for each element.
     * @param {*} [accumulator] The initial value.
     * @returns {*} The accumulated value.
     */
    function reduceRight(array, callback, accumulator) {
        var length = array == null ? 0 : array.length;
        var i = length - 1;

        if (length) {
            if (typeof callback != "function") return array[i];
            if (arguments.length < 3) accumulator = array[i--];
        }

        for (; i >= 0; i--) {
            accumulator = callback(accumulator, array[i], i, array);
        }

        return accumulator;
    }

    /**
     * Returns `string` repeated `count` times.
     *
     * @memberof module:ai-scripting-utils
     * @category String Functions
     * @param {string} string The string to repeat.
     * @param {number} [count=1] The number of repetitions.
     * @returns {string} The repeated `string`.
     */
    function repeat(string, count) {
        string = string == null ? "" : String(string);
        if (count == null) count = 1;

        var result = "";

        for (var i = 0; i < count; i++) {
            result += string;
        }

        return result;
    }

    /**
     * Rotates `item` by a given angle.
     *
     * @memberof module:ai-scripting-utils
     * @category DOM Functions
     * @param {PageItem} item The item to rotate.
     * @param {number} angle The rotation angle.
     * @param {Point} [pivotPoint] The point to rotate about. Defaults to the
     * center point of `item`.
     */
    function rotate(item, angle, pivotPoint) {
        if (!isPageItem(item) || !item.editable || isNaN(angle)) return;
        if (!pivotPoint) return item.rotate(angle);

        var rad = angle * Math.PI / 180;
        var itemCenter = getRectPoint(item.geometricBounds, "center");
        var pivotNorm = [pivotPoint[0] - itemCenter[0], pivotPoint[1] - itemCenter[1]];
        var pivotNormRotated = [
            pivotNorm[0] * Math.cos(rad) - pivotNorm[1] * Math.sin(rad),
            pivotNorm[0] * Math.sin(rad) + pivotNorm[1] * Math.cos(rad)
        ];
        var delta = subtractPoint(pivotNorm, pivotNormRotated);

        var rotationMatrix = app.getRotationMatrix(angle);
        var translationMatrix = app.getTranslationMatrix(delta[0], delta[1]);
        var transformationMatrix = app.concatenateMatrix(rotationMatrix, translationMatrix);

        item.transform(transformationMatrix);
    }

    /**
     * Creates an array of shuffled values, using the Fisher-Yates shuffle
     * algorithm.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to shuffle.
     * @returns {Array} The new shuffled array.
     */
    function shuffle(array) {
        var length = array == null ? 0 : array.length;

        if (!length) return [];

        var result = new Array(length);

        for (var i = 0; i < length; i++) {
            var j = Math.floor(Math.random() * (i + 1));
            result[i] = result[j];
            result[j] = array[i];
        }

        return result;
    }

    /**
     * Checks if `callback` returns a truly value for at least one element of
     * `array`. The callback takes three arguments: `value`, `index`, `array`.
     *
     * @memberof module:ai-scripting-utils
     * @category Array and Array-Like Functions
     * @param {Array} array The array to iterate over.
     * @param {Function} callback The function called for each element.
     * @returns {boolean} `true` if at least one element pass the check, `false`
     * otherwise.
     */
    function some(array, callback) {
        var length = array == null ? 0 : array.length;

        if (length && typeof callback != "function") return true;

        for (var i = 0; i < length; i++) {
            if (callback(array[i])) return true;
        }

        return false;
    }

    /**
     * Checks if `string` starts with `target` string.
     *
     * @memberof module:ai-scripting-utils
     * @category String Functions
     * @param {string} string The string to inspect.
     * @param {string} target The string to search for.
     * @returns {boolean} `true` if `string` starts with `target`, `false`
     * otherwise.
     */
    function startsWith(string, target) {
        string = string == null ? "" : String(string);
        return string.indexOf(target) == 0;
    }

    /**
     * Subtracts one point from another.
     *
     * @memberof module:ai-scripting-utils
     * @category Point Functions
     * @param {Point} point1 The first point.
     * @param {Point} point2 The second point.
     * @returns {Point} The result of subtracting points.
     */
    function subtractPoint(point1, point2) {
        point1 = point1 || [];
        point2 = point2 || [0, 0];
        return [point1[0] - point2[0], point1[1] - point2[1]];
    }

    /**
     * Translates `rect` by the given horizontal and vertical amount. Returns
     * the new rectangle.
     *
     * @memberof module:ai-scripting-utils
     * @category Rect Functions
     * @param {Rect} rect The rectangle to translate.
     * @param {number} dx The amount to translate horizontally.
     * @param {number} dy The amount to translate vertically.
     * @returns {Rect} The new translated rectangle.
     */
    function translateRect(rect, dx, dy) {
        rect = rect || [];
        dx = dx || 0;
        dy = dy || 0;

        return [
            +rect[0] + +dx,
            +rect[1] + +dy,
            +rect[2] + +dx,
            +rect[3] + +dy
        ];
    }

    /**
     * Removes specified characters from both ends of `string`.
     *
     * @memberof module:ai-scripting-utils
     * @category String Functions
     * @param {string} string The string to trim.
     * @param {string} [chars=​ ​] The characters to trim.
     * @returns {string} The trimmed `string`.
     */
    function trim(string, chars) {
        return trimStart(trimEnd(string, chars), chars);
    }

    /**
     * Removes specified characters from the end of `string`.
     *
     * @memberof module:ai-scripting-utils
     * @category String Functions
     * @param {string} string The string to trim.
     * @param {string} [chars=​ ​] The characters to trim.
     * @returns {string} The trimmed `string`.
     */
    function trimEnd(string, chars) {
        string = string == null ? "" : String(string);
        chars = chars == null ? " " : String(chars);
        for (var i = string.length - 1; i >= 0 && chars.indexOf(string[i]) > -1; i--);
        return string.slice(0, i + 1);
    }

    /**
     * Removes specified characters from the beginning of `string`.
     *
     * @memberof module:ai-scripting-utils
     * @category String Functions
     * @param {string} string The string to trim.
     * @param {string} [chars=​ ​] The characters to trim.
     * @returns {string} The trimmed `string`.
     */
    function trimStart(string, chars) {
        string = string == null ? "" : String(string);
        chars = chars == null ? " " : String(chars);
        for (var i = 0; i < string.length && chars.indexOf(string[i]) > -1; i++);
        return string.slice(i, string.length);
    }

    /**
     * Returns an array of own enumerable property values for `object`.
     *
     * @memberof module:ai-scripting-utils
     * @category Object Functions
     * @param {Object} object The object to query.
     * @returns {Array} The array of property values.
     */
    function values(object) {
        if (object == null || typeof object != "object") return [];

        var result = [];

        for (var key in object) {
            if (object.hasOwnProperty(key)) result.push(object[key]);
        }

        return result;
    }

    var exports = {};

    // Array and Array-Like Functions
    exports.clone = clone;
    exports.each = each;
    exports.every = every;
    exports.fill = fill;
    exports.filter = filter;
    exports.find = find;
    exports.findIndex = findIndex;
    exports.includes = includes;
    exports.indexOf = indexOf;
    exports.isArray = isArray;
    exports.isArrayLike = isArrayLike;
    exports.last = last;
    exports.lastIndexOf = lastIndexOf;
    exports.map = map;
    exports.reduce = reduce;
    exports.reduceRight = reduceRight;
    exports.shuffle = shuffle;
    exports.some = some;

    // DOM Functions
    exports.align = align;
    exports.createCMYKColor = createCMYKColor;
    exports.createGroup = createGroup;
    exports.createLayer = createLayer;
    exports.createRegistrationColor = createRegistrationColor;
    exports.createRGBColor = createRGBColor;
    exports.createSpot = createSpot;
    exports.createSpotColor = createSpotColor;
    exports.drawCircle = drawCircle;
    exports.drawEllipse = drawEllipse;
    exports.drawLine = drawLine;
    exports.drawRect = drawRect;
    exports.eachPageItem = eachPageItem;
    exports.getActiveDocument = getActiveDocument;
    exports.getLayer = getLayer;
    exports.getSpot = getSpot;
    exports.isCMYKColor = isCMYKColor;
    exports.isColor = isColor;
    exports.isCompoundPathItem = isCompoundPathItem;
    exports.isDocument = isDocument;
    exports.isDOMCollection = isDOMCollection;
    exports.isEditable = isEditable;
    exports.isGradientColor = isGradientColor;
    exports.isGraphItem = isGraphItem;
    exports.isGrayColor = isGrayColor;
    exports.isGroupItem = isGroupItem;
    exports.isLabColor = isLabColor;
    exports.isLayer = isLayer;
    exports.isLayerEditable = isLayerEditable;
    exports.isLegacyTextItem = isLegacyTextItem;
    exports.isMeshItem = isMeshItem;
    exports.isNoColor = isNoColor;
    exports.isNonNativeItem = isNonNativeItem;
    exports.isPageItem = isPageItem;
    exports.isPathItem = isPathItem;
    exports.isPatternColor = isPatternColor;
    exports.isPlacedItem = isPlacedItem;
    exports.isPluginItem = isPluginItem;
    exports.isRasterItem = isRasterItem;
    exports.isRegistrationColor = isRegistrationColor;
    exports.isRegistrationSpot = isRegistrationSpot;
    exports.isRGBColor = isRGBColor;
    exports.isSpot = isSpot;
    exports.isSpotColor = isSpotColor;
    exports.isTextFrame = isTextFrame;
    exports.move = move;
    exports.offsetFrom = offsetFrom;
    exports.rotate = rotate;

    // Object Functions
    exports.assign = assign;
    exports.defaults = defaults;
    exports.entries = entries;
    exports.keys = keys;
    exports.values = values;

    // Point Functions
    exports.addPoint = addPoint;
    exports.getDistance = getDistance;
    exports.getInclination = getInclination;
    exports.subtractPoint = subtractPoint;

    // Rect Functions
    exports.alignRect = alignRect;
    exports.createRect = createRect;
    exports.expandRect = expandRect;
    exports.getRectArea = getRectArea;
    exports.getRectHeight = getRectHeight;
    exports.getRectPoint = getRectPoint;
    exports.getRectSide = getRectSide;
    exports.getRectWidth = getRectWidth;
    exports.includeRects = includeRects;
    exports.intersectRects = intersectRects;
    exports.isRectsIntersect = isRectsIntersect;
    exports.moveRect = moveRect;
    exports.offsetRect = offsetRect;
    exports.offsetRectFrom = offsetRectFrom;
    exports.translateRect = translateRect;

    // String Functions
    exports.endsWith = endsWith;
    exports.pad = pad;
    exports.padEnd = padEnd;
    exports.padStart = padStart;
    exports.repeat = repeat;
    exports.startsWith = startsWith;
    exports.trim = trim;
    exports.trimEnd = trimEnd;
    exports.trimStart = trimStart;

    // Utility Functions
    exports.inRange = inRange;
    exports.log = log;
    exports.random = random;

    $.global.importAIScriptingUtils = function () {
        return exports;
    };

    return exports;
})();
