/* global pluploadL10n, plupload, _wpPluploadSettings, JSON */

window.wp = window.wp || {};

( function( exports, $ ) {
    var Uploader, vp;

    if ( typeof _wpPluploadSettings === 'undefined' ) {
        return;
    }

    /**
     * A WordPress uploader.
     *
     * The Plupload library provides cross-browser uploader UI integration.
     * This object bridges the Plupload API to integrate uploads into the
     * WordPress back end and the WordPress media experience.
     *
     * @param {object} options           The options passed to the new plupload instance.
     * @param {object} options.container The id of uploader container.
     * @param {object} options.browser   The id of button to trigger the file select.
     * @param {object} options.dropzone  The id of file drop target.
     * @param {object} options.plupload  An object of parameters to pass to the plupload instance.
     * @param {object} options.params    An object of parameters to pass to $_POST when uploading the file.
     *                                   Extends this.plupload.multipart_params under the hood.
     */
    Uploader = function( options ) {
        var self = this,
            isIE = navigator.userAgent.indexOf('Trident/') !== -1 || navigator.userAgent.indexOf('MSIE ') !== -1,
            elements = {
                container: 'container',
                browser:   'browse_button',
                dropzone:  'drop_element'
            },
            key, error;

        this.supports = {
            upload: Uploader.browser.supported
        };

        this.supported = this.supports.upload;

        if ( ! this.supported ) {
            return;
        }

        // Arguments to send to pluplad.Uploader().
        // Use deep extend to ensure that multipart_params and other objects are cloned.
        this.plupload = $.extend( true, { multipart_params: {} }, Uploader.defaults );
        this.container = document.body; // Set default container.

        // Extend the instance with options.
        //
        // Use deep extend to allow options.plupload to override individual
        // default plupload keys.
        $.extend( true, this, options );

        // Proxy all methods so this always refers to the current instance.
        for ( key in this ) {
            if ( $.isFunction( this[ key ] ) ) {
                this[ key ] = $.proxy( this[ key ], this );
            }
        }

        // Ensure all elements are jQuery elements and have id attributes,
        // then set the proper plupload arguments to the ids.
        for ( key in elements ) {
            if ( ! this[ key ] ) {
                continue;
            }

            this[ key ] = $( this[ key ] ).first();

            if ( ! this[ key ].length ) {
                delete this[ key ];
                continue;
            }

            if ( ! this[ key ].prop('id') ) {
                this[ key ].prop( 'id', '__wp-uploader-id-' + Uploader.uuid++ );
            }

            this.plupload[ elements[ key ] ] = this[ key ].prop('id');
        }

        // If the uploader has neither a browse button nor a dropzone, bail.
        if ( ! ( this.browser && this.browser.length ) && ! ( this.dropzone && this.dropzone.length ) ) {
            return;
        }

        // Make sure flash sends cookies (seems in IE it does without switching to urlstream mode)
        if ( ! isIE && 'flash' === plupload.predictRuntime( this.plupload ) &&
            ( ! this.plupload.required_features || ! this.plupload.required_features.hasOwnProperty( 'send_binary_string' ) ) ) {

            this.plupload.required_features = this.plupload.required_features || {};
            this.plupload.required_features.send_binary_string = true;
        }

        // Initialize the plupload instance.
        this.uploader = new plupload.Uploader( this.plupload );
        delete this.plupload;

        // Set default params and remove this.params alias.
        this.param( this.params || {} );
        delete this.params;

        // Make sure that the VideoPress object is available
        if ( typeof exports.VideoPress !== 'undefined' ) {
            vp = exports.VideoPress;

        } else {
            window.console && window.console.error( 'The VideoPress object was not loaded. Errors may occur.' );
        }

        /**
         * Custom error callback.
         *
         * Add a new error to the errors collection, so other modules can track
         * and display errors. @see wp.Uploader.errors.
         *
         * @param  {string}        message
         * @param  {object}        data
         * @param  {plupload.File} file     File that was uploaded.
         */
        error = function( message, data, file ) {
            if ( file.attachment ) {
                file.attachment.destroy();
            }

            Uploader.errors.unshift({
                message: message || pluploadL10n.default_error,
                data:    data,
                file:    file
            });

            self.error( message, data, file );
        };

        /**
         * After the Uploader has been initialized, initialize some behaviors for the dropzone.
         *
         * @param {plupload.Uploader} uploader Uploader instance.
         */
        this.uploader.bind( 'init', function( uploader ) {
            var timer, active, dragdrop,
                dropzone = self.dropzone;

            dragdrop = self.supports.dragdrop = uploader.features.dragdrop && ! Uploader.browser.mobile;

            // Generate drag/drop helper classes.
            if ( ! dropzone ) {
                return;
            }

            dropzone.toggleClass( 'supports-drag-drop', !! dragdrop );

            if ( ! dragdrop ) {
                return dropzone.unbind('.wp-uploader');
            }

            // 'dragenter' doesn't fire correctly, simulate it with a limited 'dragover'.
            dropzone.bind( 'dragover.wp-uploader', function() {
                if ( timer ) {
                    clearTimeout( timer );
                }

                if ( active ) {
                    return;
                }

                dropzone.trigger('dropzone:enter').addClass('drag-over');
                active = true;
            });

            dropzone.bind('dragleave.wp-uploader, drop.wp-uploader', function() {
                // Using an instant timer prevents the drag-over class from
                // being quickly removed and re-added when elements inside the
                // dropzone are repositioned.
                //
                // @see https://core.trac.wordpress.org/ticket/21705
                timer = setTimeout( function() {
                    active = false;
                    dropzone.trigger('dropzone:leave').removeClass('drag-over');
                }, 0 );
            });

            self.ready = true;
            $(self).trigger( 'uploader:ready' );
        });

        this.uploader.bind( 'postinit', function( up ) {
            up.refresh();
            self.init();
        });

        this.uploader.init();

        if ( this.browser ) {
            this.browser.on( 'mouseenter', this.refresh );
        } else {
            this.uploader.disableBrowse( true );
            // If HTML5 mode, hide the auto-created file container.
            $('#' + this.uploader.id + '_html5_container').hide();
        }

        /**
         * After files were filtered and added to the queue, create a model for each.
         *
         * @event FilesAdded
         * @param {plupload.Uploader} uploader Uploader instance.
         * @param {Array}             files    Array of file objects that were added to queue by the user.
         */
        this.uploader.bind( 'FilesAdded', function( up, files ) {
            _.each( files, function( file ) {
                var attributes, image;

                // Ignore failed uploads.
                if ( plupload.FAILED === file.status ) {
                    return;
                }

                // Generate attributes for a new `Attachment` model.
                attributes = _.extend({
                    file:      file,
                    uploading: true,
                    date:      new Date(),
                    filename:  file.name,
                    menuOrder: 0,
                    uploadedTo: wp.media.model.settings.post.id
                }, _.pick( file, 'loaded', 'size', 'percent' ) );

                // Handle early mime type scanning for images.
                image = /(?:jpe?g|png|gif)$/i.exec( file.name );

                // For images set the model's type and subtype attributes.
                if ( image ) {
                    attributes.type = 'image';

                    // `jpeg`, `png` and `gif` are valid subtypes.
                    // `jpg` is not, so map it to `jpeg`.
                    attributes.subtype = ( 'jpg' === image[0] ) ? 'jpeg' : image[0];
                }

                // Create a model for the attachment, and add it to the Upload queue collection
                // so listeners to the upload queue can track and display upload progress.
                file.attachment = wp.media.model.Attachment.create( attributes );
                Uploader.queue.add( file.attachment );

                self.added( file.attachment );
            });

            up.refresh();
            up.start();
        });

        this.uploader.bind( 'UploadProgress', function( up, file ) {
            file.attachment.set( _.pick( file, 'loaded', 'percent' ) );
            self.progress( file.attachment );
        });

        /**
         * After a file is successfully uploaded, update its model.
         *
         * @param {plupload.Uploader} uploader Uploader instance.
         * @param {plupload.File}     file     File that was uploaded.
         * @param {Object}            response Object with response properties.
         * @return {mixed}
         */
        this.uploader.bind( 'FileUploaded', function( up, file, response ) {
            var complete;

            try {
                response = JSON.parse( response.response );
            } catch ( e ) {
                return error( pluploadL10n.default_error, e, file );
            }

            if ( typeof response.media !== 'undefined' ) {
                response = vp.handleRestApiResponse( response, file );
            } else {
                response = vp.handleStandardResponse( response, file );
            }

            _.each(['file','loaded','size','percent'], function( key ) {
                file.attachment.unset( key );
            });

            file.attachment.set( _.extend( response.data, { uploading: false }) );
            wp.media.model.Attachment.get( response.data.id, file.attachment );

            complete = Uploader.queue.all( function( attachment ) {
                return ! attachment.get('uploading');
            });

            if ( complete ) {
                vp && vp.resetToOriginalOptions( up );
                Uploader.queue.reset();
            }

            self.success( file.attachment );
        });

        /**
         * When plupload surfaces an error, send it to the error handler.
         *
         * @param {plupload.Uploader} uploader Uploader instance.
         * @param {Object}            error    Contains code, message and sometimes file and other details.
         */
        this.uploader.bind( 'Error', function( up, pluploadError ) {
            var message = pluploadL10n.default_error,
                key;

            // Check for plupload errors.
            for ( key in Uploader.errorMap ) {
                if ( pluploadError.code === plupload[ key ] ) {
                    message = Uploader.errorMap[ key ];

                    if ( _.isFunction( message ) ) {
                        message = message( pluploadError.file, pluploadError );
                    }

                    break;
                }
            }

            error( message, pluploadError, pluploadError.file );
            vp && vp.resetToOriginalOptions( up );
            up.refresh();
        });

        /**
         * Add in a way for the uploader to reset itself when uploads are complete.
         */
        this.uploader.bind( 'UploadComplete', function( up ) {
            vp && vp.resetToOriginalOptions( up );
        });

        /**
         * Before we upload, check to see if this file is a videopress upload, if so, set new options and save the old ones.
         */
        this.uploader.bind( 'BeforeUpload', function( up, file ) {
            if ( typeof file.videopress !== 'undefined' ) {
                vp.originalOptions.url = up.getOption( 'url' );
                vp.originalOptions.multipart_params = up.getOption( 'multipart_params' );
                vp.originalOptions.file_data_name = up.getOption( 'file_data_name' );

                up.setOption( 'file_data_name', 'media[]' );
                up.setOption( 'url', file.videopress.upload_action_url );
                up.setOption( 'headers', {
                    Authorization: 'X_UPLOAD_TOKEN token="' + file.videopress.upload_token + '" blog_id="' + file.videopress.upload_blog_id + '"'
                });
            }
        });
    };

    // Adds the 'defaults' and 'browser' properties.
    $.extend( Uploader, _wpPluploadSettings );

    Uploader.uuid = 0;

    // Map Plupload error codes to user friendly error messages.
    Uploader.errorMap = {
        'FAILED':                 pluploadL10n.upload_failed,
        'FILE_EXTENSION_ERROR':   pluploadL10n.invalid_filetype,
        'IMAGE_FORMAT_ERROR':     pluploadL10n.not_an_image,
        'IMAGE_MEMORY_ERROR':     pluploadL10n.image_memory_exceeded,
        'IMAGE_DIMENSIONS_ERROR': pluploadL10n.image_dimensions_exceeded,
        'GENERIC_ERROR':          pluploadL10n.upload_failed,
        'IO_ERROR':               pluploadL10n.io_error,
        'HTTP_ERROR':             pluploadL10n.http_error,
        'SECURITY_ERROR':         pluploadL10n.security_error,

        'FILE_SIZE_ERROR': function( file ) {
            return pluploadL10n.file_exceeds_size_limit.replace('%s', file.name);
        }
    };

    $.extend( Uploader.prototype, {
        /**
         * Acts as a shortcut to extending the uploader's multipart_params object.
         *
         * param( key )
         *    Returns the value of the key.
         *
         * param( key, value )
         *    Sets the value of a key.
         *
         * param( map )
         *    Sets values for a map of data.
         */
        param: function( key, value ) {
            if ( arguments.length === 1 && typeof key === 'string' ) {
                return this.uploader.settings.multipart_params[ key ];
            }

            if ( arguments.length > 1 ) {
                this.uploader.settings.multipart_params[ key ] = value;
            } else {
                $.extend( this.uploader.settings.multipart_params, key );
            }
        },

        /**
         * Make a few internal event callbacks available on the wp.Uploader object
         * to change the Uploader internals if absolutely necessary.
         */
        init:     function() {},
        error:    function() {},
        success:  function() {},
        added:    function() {},
        progress: function() {},
        complete: function() {},
        refresh:  function() {
            var node, attached, container, id;

            if ( this.browser ) {
                node = this.browser[0];

                // Check if the browser node is in the DOM.
                while ( node ) {
                    if ( node === document.body ) {
                        attached = true;
                        break;
                    }
                    node = node.parentNode;
                }

                // If the browser node is not attached to the DOM, use a
                // temporary container to house it, as the browser button
                // shims require the button to exist in the DOM at all times.
                if ( ! attached ) {
                    id = 'wp-uploader-browser-' + this.uploader.id;

                    container = $( '#' + id );
                    if ( ! container.length ) {
                        container = $('<div class="wp-uploader-browser" />').css({
                            position: 'fixed',
                            top: '-1000px',
                            left: '-1000px',
                            height: 0,
                            width: 0
                        }).attr( 'id', 'wp-uploader-browser-' + this.uploader.id ).appendTo('body');
                    }

                    container.append( this.browser );
                }
            }

            this.uploader.refresh();
        }
    });

    // Create a collection of attachments in the upload queue,
    // so that other modules can track and display upload progress.
    Uploader.queue = new wp.media.model.Attachments( [], { query: false });

    // Create a collection to collect errors incurred while attempting upload.
    Uploader.errors = new Backbone.Collection();

    exports.Uploader = Uploader;
})( wp, jQuery );
