// Version: 2.8.2
// https://github.com/DevExpress/DevExtreme.AspNet.Data
// Copyright (c) Developer Express Inc.

/* global DevExpress:false, jQuery:false */

(function(factory) {
    "use strict";

    if(typeof define === "function" && define.amd) {
        define(function(require, exports, module) {
            module.exports = factory(
                require("devextreme/core/utils/ajax"),
                require("jquery").Deferred,
                require("jquery").extend,
                require("devextreme/data/custom_store"),
                require("devextreme/data/utils")
            );
        });
    } else if (typeof module === "object" && module.exports) {
        module.exports = factory(
            require("devextreme/core/utils/ajax"),
            require("jquery").Deferred,
            require("jquery").extend,
            require("devextreme/data/custom_store"),
            require("devextreme/data/utils")
        );
    } else {
        DevExpress.data.AspNet = factory(
            DevExpress.utils.ajax || { sendRequest: jQuery.ajax },
            jQuery.Deferred,
            jQuery.extend,
            DevExpress.data.CustomStore,
            DevExpress.data.utils
        );
    }

})(function(ajaxUtility, Deferred, extend, CustomStore, dataUtils) {
    "use strict";

    var CUSTOM_STORE_OPTIONS = [
        "onLoading", "onLoaded",
        "onInserting", "onInserted",
        "onUpdating", "onUpdated",
        "onRemoving", "onRemoved",
        "onModifying", "onModified",
        "onPush",
        "loadMode", "cacheRawData",
        "errorHandler"
    ];

    function createStoreConfig(options) {
        var keyExpr = options.key,
            loadUrl = options.loadUrl,
            loadMethod = options.loadMethod || "GET",
            loadParams = options.loadParams,
            isRawLoadMode = options.loadMode === "raw",
            updateUrl = options.updateUrl,
            insertUrl = options.insertUrl,
            deleteUrl = options.deleteUrl,
            onBeforeSend = options.onBeforeSend,
            onAjaxError = options.onAjaxError;

        function send(operation, requiresKey, ajaxSettings, customSuccessHandler) {
            var d = Deferred(),
                thenable,
                beforeSendResult;

            function sendCore() {
                ajaxUtility.sendRequest(ajaxSettings).then(
                    function(res, textStatus, xhr) {
                        if(customSuccessHandler)
                            customSuccessHandler(d, res, xhr);
                        else
                            d.resolve();
                    },
                    function(xhr, textStatus) {
                        var error = getErrorMessageFromXhr(xhr);

                        if(onAjaxError) {
                            var e = { xhr: xhr, error: error };
                            onAjaxError(e);
                            error = e.error;
                        }

                        if(error)
                            d.reject(error);
                        else
                            d.reject(xhr, textStatus);
                    }
                );
            }

            if(requiresKey && !keyExpr) {
                d.reject(new Error("Primary key is not specified (operation: '" + operation + "', url: '" + ajaxSettings.url + "')"));
            } else {
                if(operation === "load") {
                    ajaxSettings.cache = false;
                    ajaxSettings.dataType = "json";
                } else {
                    ajaxSettings.dataType = "text";
                }

                if(onBeforeSend) {
                    beforeSendResult = onBeforeSend(operation, ajaxSettings);
                    if(beforeSendResult && typeof beforeSendResult.then === "function")
                        thenable = beforeSendResult;
                }

                if(thenable)
                    thenable.then(sendCore, function(error) { d.reject(error); });
                else
                    sendCore();
            }

            return d.promise();
        }

        function filterByKey(keyValue) {
            if(!Array.isArray(keyExpr))
                return [keyExpr, keyValue];

            return keyExpr.map(function(i) {
                return [i, keyValue[i]];
            });
        }

        function loadOptionsToActionParams(options, isCountQuery) {
            var result = {};

            if(isCountQuery)
                result.isCountQuery = isCountQuery;

            if(options) {

                ["skip", "take", "requireTotalCount", "requireGroupCount"].forEach(function(i) {
                    if(options[i] !== undefined)
                        result[i] = options[i];
                });

                var normalizeSorting = dataUtils.normalizeSortingInfo,
                    group = options.group,
                    filter = options.filter,
                    select = options.select;

                if(options.sort)
                    result.sort = JSON.stringify(normalizeSorting(options.sort));

                if(group) {
                    if(!isAdvancedGrouping(group))
                        group = normalizeSorting(group);
                    result.group = JSON.stringify(group);
                }

                if(Array.isArray(filter)) {
                    filter = extend(true, [], filter);
                    stringifyDatesInFilter(filter);
                    result.filter = JSON.stringify(filter);
                }

                if(options.totalSummary)
                    result.totalSummary = JSON.stringify(options.totalSummary);

                if(options.groupSummary)
                    result.groupSummary = JSON.stringify(options.groupSummary);

                if(select) {
                    if(!Array.isArray(select))
                        select = [ select ];
                    result.select = JSON.stringify(select);
                }
            }

            extend(result, loadParams);

            return result;
        }

        function handleInsertUpdateSuccess(d, res, xhr) {
            var mime = xhr.getResponseHeader("Content-Type"),
                isJSON = mime && mime.indexOf("application/json") > -1;
            d.resolve(isJSON ? JSON.parse(res) : res);
        }

        var result = {
            key: keyExpr,
            useDefaultSearch: true,

            load: function(loadOptions) {
                return send(
                    "load",
                    false,
                    {
                        url: loadUrl,
                        method: loadMethod,
                        data: loadOptionsToActionParams(loadOptions)
                    },
                    function(d, res) {
                        processLoadResponse(d, res, function(res) {
                            return [ res.data, createLoadExtra(res) ];
                        });
                    }
                );
            },

            totalCount: !isRawLoadMode && function(loadOptions) {
                return send(
                    "load",
                    false,
                    {
                        url: loadUrl,
                        method: loadMethod,
                        data: loadOptionsToActionParams(loadOptions, true)
                    },
                    function(d, res) {
                        processLoadResponse(d, res, function(res) {
                            return [ res.totalCount ];
                        });
                    }
                );
            },

            byKey: !isRawLoadMode && function(key) {
                return send(
                    "load",
                    true,
                    {
                        url: loadUrl,
                        method: loadMethod,
                        data: loadOptionsToActionParams({ filter: filterByKey(key) })
                    },
                    function(d, res) {
                        processLoadResponse(d, res, function(res) {
                            return [ res.data[0] ];
                        });
                    }
                );
            },

            update: updateUrl && function(key, values) {
                return send(
                    "update",
                    true,
                    {
                        url: updateUrl,
                        method: options.updateMethod || "PUT",
                        data: {
                            key: serializeKey(key),
                            values: JSON.stringify(values)
                        }
                    },
                    handleInsertUpdateSuccess
                );
            },

            insert: insertUrl && function(values) {
                return send(
                    "insert",
                    true,
                    {
                        url: insertUrl,
                        method: options.insertMethod || "POST",
                        data: { values: JSON.stringify(values) }
                    },
                    handleInsertUpdateSuccess
                );
            },

            remove: deleteUrl && function(key) {
                return send("delete", true, {
                    url: deleteUrl,
                    method: options.deleteMethod || "DELETE",
                    data: { key: serializeKey(key) }
                });
            }

        };

        CUSTOM_STORE_OPTIONS.forEach(function(name) {
            var value = options[name];
            if(value !== undefined)
                result[name] = value;
        });

        return result;
    }

    function processLoadResponse(d, res, getResolveArgs) {
        res = expandLoadResponse(res);

        if(!res || typeof res !== "object")
            d.reject(new Error("Unexpected response received"));
        else
            d.resolve.apply(d, getResolveArgs(res));
    }

    function expandLoadResponse(value) {
        if(Array.isArray(value))
            return { data: value };

        if(typeof value === "number")
            return { totalCount: value };

        return value;
    }

    function createLoadExtra(res) {
        return {
            totalCount: "totalCount" in res ? res.totalCount : -1,
            groupCount: "groupCount" in res ? res.groupCount : -1,
            summary: res.summary || null
        };
    }

    function serializeKey(key) {
        if(typeof key === "object")
            return JSON.stringify(key);

        return key;
    }

    function serializeDate(date) {

        function zpad(text, len) {
            text = String(text);
            while(text.length < len)
                text = "0" + text;
            return text;
        }

        var builder = [1 + date.getMonth(), "/", date.getDate(), "/", date.getFullYear()],
            h = date.getHours(),
            m = date.getMinutes(),
            s = date.getSeconds(),
            f = date.getMilliseconds();

        if(h + m + s + f > 0)
            builder.push(" ", zpad(h, 2), ":", zpad(m, 2), ":", zpad(s, 2), ".", zpad(f, 3));

        return builder.join("");
    }

    function stringifyDatesInFilter(crit) {
        crit.forEach(function(v, k) {
            if(Array.isArray(v)) {
                stringifyDatesInFilter(v);
            } else if(Object.prototype.toString.call(v) === "[object Date]") {
                crit[k] = serializeDate(v);
            }
        });
    }

    function isAdvancedGrouping(expr) {
        if(!Array.isArray(expr))
            return false;

        for(var i = 0; i < expr.length; i++) {
            if("groupInterval" in expr[i] || "isExpanded" in expr[i])
                return true;
        }

        return false;
    }

    function getErrorMessageFromXhr(xhr) {
        var mime = xhr.getResponseHeader("Content-Type"),
            responseText = xhr.responseText;

        if(!mime)
            return null;

        if(mime.indexOf("text/plain") === 0)
            return responseText;

        if(mime.indexOf("application/json") === 0) {
            var jsonObj = safeParseJSON(responseText);

            if(typeof jsonObj === "string")
                return jsonObj;

            if(typeof jsonObj === "object") {
                for(var key in jsonObj) {
                    if(typeof jsonObj[key] === "string")
                        return jsonObj[key];
                }
            }

            return responseText;
        }

        return null;
    }

    function safeParseJSON(json) {
        try {
            return JSON.parse(json);
        } catch(x) {
            return null;
        }
    }

    return {
        createStore: function(options) {
            return new CustomStore(createStoreConfig(options));
        }
    };
});
