/*globals qq, File, XMLHttpRequest, FormData*/
qq.UploadHandlerXhr = function(o, uploadCompleteCallback, logCallback) {
    "use strict";
    
    var options = o,
        uploadComplete = uploadCompleteCallback,
        log = logCallback,
        fileState = [],
        cookieItemDelimiter = "|",
        chunkFiles = options.chunking.enabled && qq.isFileChunkingSupported(),
        resumeEnabled = options.resume.enabled && chunkFiles && qq.areCookiesEnabled(),
        resumeId = getResumeId(),
        multipart = options.forceMultipart || options.paramsInBody,
        api;


     function addChunkingSpecificParams(id, params, chunkData) {
        var size = api.getSize(id),
            name = api.getName(id);

        params[options.chunking.paramNames.partIndex] = chunkData.part;
        params[options.chunking.paramNames.partByteOffset] = chunkData.start;
        params[options.chunking.paramNames.chunkSize] = chunkData.end - chunkData.start;
        params[options.chunking.paramNames.totalParts] = chunkData.count;
        params[options.totalFileSizeParamName] = size;


        /**
         * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
         * or an empty string.  So, we will need to include the actual file name as a param in this case.
         */
        if (multipart) {
            params[options.chunking.paramNames.filename] = name;
        }
    }

    function addResumeSpecificParams(params) {
        params[options.resume.paramNames.resuming] = true;
    }

     function getChunk(file, startByte, endByte) {
        if (file.slice) {
            return file.slice(startByte, endByte);
        }
        else if (file.mozSlice) {
            return file.mozSlice(startByte, endByte);
        }
        else if (file.webkitSlice) {
            return file.webkitSlice(startByte, endByte);
        }
    }

    function getChunkData(id, chunkIndex) {
        var chunkSize = options.chunking.partSize,
            fileSize = api.getSize(id),
            file = fileState[id].file,
            startBytes = chunkSize * chunkIndex,
            endBytes = startBytes+chunkSize >= fileSize ? fileSize : startBytes+chunkSize,
            totalChunks = getTotalChunks(id);

        return {
            part: chunkIndex,
            start: startBytes,
            end: endBytes,
            count: totalChunks,
            blob: getChunk(file, startBytes, endBytes)
        };
    }

    function getTotalChunks(id) {
        var fileSize = api.getSize(id),
            chunkSize = options.chunking.partSize;

        return Math.ceil(fileSize / chunkSize);
    }

    function createXhr(id) {
        fileState[id].xhr = new XMLHttpRequest();
        return fileState[id].xhr;
    }

    function setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id) {
        var formData = new FormData(),
            protocol = options.demoMode ? "GET" : "POST",
            endpoint = options.endpointStore.getEndpoint(id),
            url = endpoint,
            name = api.getName(id),
            size = api.getSize(id);

        params[options.uuidParamName] = fileState[id].uuid;

        if (multipart) {
            params[options.totalFileSizeParamName] = size;
        }

        //build query string
        if (!options.paramsInBody) {
            params[options.inputName] = name;
            url = qq.obj2url(params, endpoint);
        }

        xhr.open(protocol, url, true);
        if (multipart) {
            if (options.paramsInBody) {
                qq.obj2FormData(params, formData);
            }

            formData.append(options.inputName, fileOrBlob);
            return formData;
        }

        return fileOrBlob;
    }

    function setHeaders(id, xhr) {
        log('setHeaders');
        var extraHeaders = options.customHeaders,
            name = api.getName(id),
            file = fileState[id].file;

        xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
        xhr.setRequestHeader("Cache-Control", "no-cache");

        if (!multipart) {
            xhr.setRequestHeader("Content-Type", "application/octet-stream");
            //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
            xhr.setRequestHeader("X-Mime-Type", file.type);
        }
        qq.each(extraHeaders, function(name, val) {
            xhr.setRequestHeader(name, val);
        });
    }

    function handleCompletedFile(id, response, xhr) {
        var name = api.getName(id),
            size = api.getSize(id);

        fileState[id].attemptingResume = false;

        options.onProgress(id, name, size, size);

        options.onComplete(id, name, response, xhr);
        delete fileState[id].xhr;
        uploadComplete(id);
    }

    function uploadNextChunk(id) {
        var chunkData = getChunkData(id, fileState[id].remainingChunkIdxs[0]),
            xhr = createXhr(id),
            size = api.getSize(id),
            name = api.getName(id),
            toSend, params;

        if (fileState[id].loaded === undefined) {
            fileState[id].loaded = 0;
        }

        persistChunkData(id, chunkData);

        xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);

        xhr.upload.onprogress = function(e) {
            if (e.lengthComputable) {
                if (fileState[id].loaded < size) {
                    var totalLoaded = e.loaded + fileState[id].loaded;
                    options.onProgress(id, name, totalLoaded, size);
                }
            }
        };

        options.onUploadChunk(id, name, getChunkDataForCallback(chunkData));

        params = options.paramsStore.getParams(id);
        addChunkingSpecificParams(id, params, chunkData);

        if (fileState[id].attemptingResume) {
            addResumeSpecificParams(params);
        }

        toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id);
        setHeaders(id, xhr);

        log('Sending chunked upload request for ' + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size);
        xhr.send(toSend);
    }


     function handleSuccessfullyCompletedChunk(id, response, xhr) {
        var chunkIdx = fileState[id].remainingChunkIdxs.shift(),
            chunkData = getChunkData(id, chunkIdx);

        fileState[id].attemptingResume = false;
        fileState[id].loaded += chunkData.end - chunkData.start;

        if (fileState[id].remainingChunkIdxs.length > 0) {
            uploadNextChunk(id);
        }
        else {
            deletePersistedChunkData(id);
            handleCompletedFile(id, response, xhr);
        }
    }

    function isErrorResponse(xhr, response) {
        return xhr.status !== 200 || !response.success || response.reset;
    }

    function parseResponse(xhr) {
        var response;

        try {
            response = qq.parseJson(xhr.responseText);
        }
        catch(error) {
            log('Error when attempting to parse xhr response text (' + error + ')', 'error');
            response = {};
        }

        return response;
    }

    function handleResetResponse(id) {
        log('Server has ordered chunking effort to be restarted on next attempt for file ID ' + id, 'error');

        if (resumeEnabled) {
            deletePersistedChunkData(id);
        }
        fileState[id].remainingChunkIdxs = [];
        delete fileState[id].loaded;
    }

    function handleResetResponseOnResumeAttempt(id) {
        fileState[id].attemptingResume = false;
        log("Server has declared that it cannot handle resume for file ID " + id + " - starting from the first chunk", 'error');
        api.upload(id, true);
    }

    function handleNonResetErrorResponse(id, response, xhr) {
        var name = api.getName(id);

        if (options.onAutoRetry(id, name, response, xhr)) {
            return;
        }
        else {
            handleCompletedFile(id, response, xhr);
        }
    }

    function onComplete(id, xhr) {
        var response;

        // the request was aborted/cancelled
        if (!fileState[id]) {
            return;
        }

        log("xhr - server response received for " + id);
        log("responseText = " + xhr.responseText);
        response = parseResponse(xhr);

        if (isErrorResponse(xhr, response)) {
            if (response.reset) {
                handleResetResponse(id);
            }

            if (fileState[id].attemptingResume && response.reset) {
                handleResetResponseOnResumeAttempt(id);
            }
            else {
                handleNonResetErrorResponse(id, response, xhr);
            }
        }
        else if (chunkFiles) {
            handleSuccessfullyCompletedChunk(id, response, xhr);
        }
        else {
            handleCompletedFile(id, response, xhr);
        }
    }

    function getChunkDataForCallback(chunkData) {
        return {
            partIndex: chunkData.part,
            startByte: chunkData.start + 1,
            endByte: chunkData.end,
            totalParts: chunkData.count
        };
    }

    function getReadyStateChangeHandler(id, xhr) {
        return function() {
            if (xhr.readyState === 4) {
                onComplete(id, xhr);
            }
        };
    }

    function persistChunkData(id, chunkData) {
        var fileUuid = api.getUuid(id),
            cookieName = getChunkDataCookieName(id),
            cookieValue = fileUuid + cookieItemDelimiter + chunkData.part,
            cookieExpDays = options.resume.cookiesExpireIn;

        qq.setCookie(cookieName, cookieValue, cookieExpDays);
    }

    function deletePersistedChunkData(id) {
        var cookieName = getChunkDataCookieName(id);

        qq.deleteCookie(cookieName);
    }

    function getPersistedChunkData(id) {
        var chunkCookieValue = qq.getCookie(getChunkDataCookieName(id)),
            delimiterIndex, uuid, partIndex;

        if (chunkCookieValue) {
            delimiterIndex = chunkCookieValue.indexOf(cookieItemDelimiter);
            uuid = chunkCookieValue.substr(0, delimiterIndex);
            partIndex = parseInt(chunkCookieValue.substr(delimiterIndex + 1, chunkCookieValue.length - delimiterIndex), 10);

            return {
                uuid: uuid,
                part: partIndex
            };
        }
    }

    function getChunkDataCookieName(id) {
        var filename = api.getName(id),
            fileSize = api.getSize(id),
            maxChunkSize = options.chunking.partSize,
            cookieName;

        cookieName = "qqfilechunk" + cookieItemDelimiter + encodeURIComponent(filename) + cookieItemDelimiter + fileSize + cookieItemDelimiter + maxChunkSize;

        if (resumeId !== undefined) {
            cookieName += cookieItemDelimiter + resumeId;
        }

        return cookieName;
    }

    function getResumeId() {
        if (options.resume.id !== null &&
            options.resume.id !== undefined &&
            !qq.isFunction(options.resume.id) &&
            !qq.isObject(options.resume.id)) {

            return options.resume.id;
        }
    }

    function handleFileChunkingUpload(id, retry) {
        var name = api.getName(id),
            firstChunkIndex = 0,
            persistedChunkInfoForResume, firstChunkDataForResume, currentChunkIndex;

        if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) {
            fileState[id].remainingChunkIdxs = [];

            if (resumeEnabled && !retry) {
                persistedChunkInfoForResume = getPersistedChunkData(id);
                if (persistedChunkInfoForResume) {
                    firstChunkDataForResume = getChunkData(id, persistedChunkInfoForResume.part);
                    if (options.onResume(id, name, getChunkDataForCallback(firstChunkDataForResume)) !== false) {
                        firstChunkIndex = persistedChunkInfoForResume.part;
                        fileState[id].uuid = persistedChunkInfoForResume.uuid;
                        fileState[id].loaded = firstChunkDataForResume.start;
                        fileState[id].attemptingResume = true;
                        log('Resuming ' + name + " at partition index " + firstChunkIndex);
                    }
                }
            }

            for (currentChunkIndex = getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) {
                fileState[id].remainingChunkIdxs.unshift(currentChunkIndex);
            }
        }

        uploadNextChunk(id);
    }

    function handleStandardFileUpload(id) {
        var file = fileState[id].file,
            name = api.getName(id),
            xhr, params, toSend;

        fileState[id].loaded = 0;

        xhr = createXhr(id);

        xhr.upload.onprogress = function(e){
            if (e.lengthComputable){
                fileState[id].loaded = e.loaded;
                options.onProgress(id, name, e.loaded, e.total);
            }
        };

        xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);

        params = options.paramsStore.getParams(id);
        toSend = setParamsAndGetEntityToSend(params, xhr, file, id);
        setHeaders(id, xhr);

        log('Sending upload request for ' + id);
        xhr.send(toSend);
    }


    api = {
        /**
         * Adds file to the queue
         * Returns id to use with upload, cancel
         **/

        add: function(file){
            if (!(file instanceof File)){
                throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
            }


            var id = fileState.push({file: file}) - 1;
            fileState[id].uuid = qq.getUniqueId();

            return id;
        },
        getName: function(id){
            var file = fileState[id].file;
            // fix missing name in Safari 4
            //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
            return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
        },
        getSize: function(id){
            /*jshint eqnull: true*/
            var file = fileState[id].file;
            return file.fileSize != null ? file.fileSize : file.size;
        },
        getFile: function(id) {
            if (fileState[id]) {
                return fileState[id].file;
            }
        },
        /**
         * Returns uploaded bytes for file identified by id
         */
        getLoaded: function(id){
            return fileState[id].loaded || 0;
        },
        isValid: function(id) {
            return fileState[id] !== undefined;
        },
        reset: function() {
            fileState = [];
        },
        getUuid: function(id) {
            return fileState[id].uuid;
        },
        /**
         * Sends the file identified by id to the server
         */
        upload: function(id, retry){
            var name = this.getName(id);

            options.onUpload(id, name);

            if (chunkFiles) {
                handleFileChunkingUpload(id, retry);
            }
            else {
                handleStandardFileUpload(id);
            }
        },
        cancel: function(id){
            options.onCancel(id, this.getName(id));

            if (fileState[id].xhr){
                fileState[id].xhr.abort();
            }

            if (resumeEnabled) {
                deletePersistedChunkData(id);
            }

            delete fileState[id];
        },
        getResumableFilesData: function() {
            var matchingCookieNames = [],
                resumableFilesData = [];

            if (chunkFiles && resumeEnabled) {
                if (resumeId === undefined) {
                    matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
                        cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "="));
                }
                else {
                    matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
                        cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "\\" +
                        cookieItemDelimiter + resumeId + "="));
                }

                qq.each(matchingCookieNames, function(idx, cookieName) {
                    var cookiesNameParts = cookieName.split(cookieItemDelimiter);
                    var cookieValueParts = qq.getCookie(cookieName).split(cookieItemDelimiter);

                    resumableFilesData.push({
                        name: decodeURIComponent(cookiesNameParts[1]),
                        size: cookiesNameParts[2],
                        uuid: cookieValueParts[0],
                        partIdx: cookieValueParts[1]
                    });
                });

                return resumableFilesData;
            }
            return [];
        }
    };

    return api;
};
