// form config.js
/* global uiLocale, urlVersions */
// from core.js
/* global isEmpty, deleteFromRest, fillSelect, MessageTypesEnum, showFeedbackMessage, getCurrentVersionData */
/* global getFromRestWithParams, getFromRest */
/* global postToRestWithVersionAndParams, putToRestWithVersionAndParams */

// TODO: settings for table selection mode (single, multiple)

var odinMetaList = "/meta/list";
var odinMetaElement = "/meta/element";
var odinCopy = "/copy-from-version";

/* exported OdinTableWithMeta */
function OdinTableWithMeta(divId, dataUrl, disableCopy) {
    return OdinTableWithMetaAndParams(divId, dataUrl, "", disableCopy, "");
}

/* exported OdinTableWithMetaAndGlobalCallback */
function OdinTableWithMetaAndGlobalCallback(divId, dataUrl, disableCopy, globalSaveCallback, globalSelectCallback) {
    return OdinTableWithMetaAndParams(divId, dataUrl, "", disableCopy, globalSaveCallback, globalSelectCallback);
}

/* exported OdinTableWithMetaAndParams */
function OdinTableWithMetaAndParams(divId, dataUrl, params, disableCopy, globalSaveCallback, globalSelectCallback) {
    return new OdinTable(divId, dataUrl + odinMetaList, dataUrl + odinMetaElement, dataUrl,
        params, disableCopy, globalSaveCallback, globalSelectCallback);
}

function OdinTable(divId, listMetaDataUrl, elementMetaDataUrl, dataUrl, params, disableCopy, globalSaveCallback, globalSelectCallback) {
    var CLICK_DELAY = 250;
    var DATA_ATTR = "odin-data";
    var DATA_VIEW_ATTR = "odin-data-view";
    var TYPE_ATTR = "type";
    var ID_ATTR = "id";
    var DISABLED_ATTR = "disabled";
    var REQUIRED_ATTR = "required";
    var LABELS_SELECTOR = "label";
    var INPUTS_SELECTOR = ":input";
    var BUTTONS_SELECTOR = ":button";
    var CHECKED_ATTR = "checked";
    var NOT_CHECKED_ATTR = "";
    var PAGINATOR_ITEMS_PER_PAGE = 10;
    var PAGINATOR_VISIBLE_BUTTONS_COUNT = 3;
    var odinTable;

    var L10nEnum = {
        VIEW: "Наименование",
        COLLECTION: "Коллеция объектов",
        GO_TO: "Перейти",

        BUTTON_ADD: "Добавить",
        BUTTON_CREATE: "Создать",
        BUTTON_EDIT: "Изменить",
        BUTTON_DELETE: "Удалить",

        BUTTON_COPY: "Копировать",

        BUTTON_OK: "Продолжить",
        BUTTON_CANCEL: "Отмена",

        BUTTON_SAVE: "Записать",
        BUTTON_CLOSE: "Закрыть",

        CAPTION_CREATE_FORM: "Новый элемент",
        CAPTION_EDIT_FORM: "(редактирование)",
        CAPTION_CHOOSE_FORM: "Выбор объекта",
        CAPTION_COPY_FORM: "Копирование данных",
        CAPTION_DEFAULT_TAB: "Реквизиты",
        CPATION_FORM_DEFAULT: "...",

        QUESTION_LOST_CHANGES: "Закрыть без сохранения?",
        QUESTION_DELETE_ITEM: "Выполнить удаление?",
        QUESTION_DEFAULT_CAPTION: "Продолжит?",

        ERROR_VERSIONS_MATCH: "Версии совпадают"
    };
    Object.freeze(L10nEnum);

    var FieldTypesEnum = {
        BOOLEAN: "boolean",
        DATE: "date",
        NUMERIC: "numeric",
        STRING: "string",
        COLLECTION: "collection",
        OBJECT: "object"
    };
    Object.freeze(FieldTypesEnum);

    var DateTypesEnum = {
        DATETIME: "datetime",
        DATE: "date",
        TIME: "time"
    };
    Object.freeze(DateTypesEnum);

    var StringTypesEnum = {
        STRING: "string",
        PASSWORD: "password",
        TEXT: "text",
        EMAIL: "email",
        HREF: "href"
    };
    Object.freeze(StringTypesEnum);

    var VisibleTypesEnum = {
        ALL: "all",
        ON_CREATE: "on_create",
        ON_UPDATE: "on_update",
        NONE: "none"
    };
    Object.freeze(VisibleTypesEnum);

    var InputTypesEnum = {
        CHECKBOX: "checkbox",
        PASSWORD: "password",
        TEXT: "text",
        EMAIL: "email",
        DATE: "date",
        DATETIME: "datetime-local",
        TIME: "time",
        NUMBER: "number",
        HIDDEN: "hidden",
        SUBMIT: "submit"
    };
    Object.freeze(InputTypesEnum);

    var FormTypesEnum = {
        FORM: "form",
        CONFIRM: "confirm",
        SIMPLE: "simple"
    };
    Object.freeze(FormTypesEnum);

    var NumberAttrEnum = {
        MIN: "min",
        MAX: "max",
        STEP: "step"
    };
    Object.freeze(NumberAttrEnum);

    var StringAttrEnum = {
        MIN: "minlength",
        MAX: "maxlength"
    };
    Object.freeze(StringAttrEnum);

    $(document.body).append("<div id=\"odinTemplates\"></div>");
    $("#odinTemplates").load("/templates/odin.html", function () {
        $("#" + divId).append("<div id='preloader'>" +
            "    <i class='fa fa-spinner fa-spin'></i>" +
            "</div>");
        odinTable = new OdinTableMain(divId, listMetaDataUrl, elementMetaDataUrl, dataUrl, params, disableCopy, globalSaveCallback, globalSelectCallback);
    });

    this.getSelectedItems = function () {
        return odinTable.getSelectedItems();
    }

    function format(str, args) {
        if (isEmpty(str) || isEmpty(args)) {
            return str;
        }
        if (!Array.isArray(args)) {
            throw "Odin: Format args is not array";
        }
        return str.replace(/\{(\d+)\}/g,
            function (match, number) {
                return typeof args[number] !== "undefined" ?
                    args[number] : match;
            });
    }

    function centerDiv(component) {
        if (isEmpty(component)) {
            return;
        }
        component.offset({
            top: ($(window).height() - component.height()) / 2,
            left: ($(window).width() - component.width()) / 2
        });
    }

    function tmpl(id) {
        var html = $("#odinTemplates").find("#" + id).html();
        if (isEmpty(html)) {
            throw "Odin: Template load error";
        }
        var template = format(html.trim(), Array.prototype.slice.call(arguments, 1));
        return $(template);
    }

    function find(object, selector) {
        var result = object.find(selector);
        if (isEmpty(result)) {
            throw "Can't find elements by selector " + selector;
        }
        return result;
    }

    function findFiltered(object, selector, notSelector) {
        var result = object.find(selector).not(notSelector);
        if (isEmpty(result)) {
            throw "Can't find elements by selector " + selector;
        }
        return result;
    }

    function OdinFormCommon(type) {
        var BUTTON_CLOSE_SELECTOR = "i.fa";
        var CAPTION_SELECTOR = "#caption";
        var PANEL_SELECTOR = ".panel";
        var FORM_INPUTS_SELECTOR = ":input";
        var DATA_CHANGED_ATTR = "changed";

        var form;
        var header;
        var content;
        var footer;

        var createForm = function () {
            switch (type) {
                case FormTypesEnum.FORM:
                    form = tmpl("form");
                    break;
                case FormTypesEnum.CONFIRM:
                    form = tmpl("formConfirm");
                    break;
                default:
                    form = tmpl("formSimple");
            }
            if (isEmpty(form)) {
                throw "Odin: Unknown form type";
            }
            header = createFormHeader();
            content = createFormContent();
            footer = createFormFooter();
            var formPanel = tmpl("formPanel");
            formPanel.append(header);
            formPanel.append(content);
            formPanel.append(footer);
            form.append(formPanel);
            setCaption(L10nEnum.CPATION_FORM_DEFAULT);
            $(document.body).append(form);
        };

        var createFormHeader = function () {
            var fHeader = tmpl("formHeader");
            fHeader.find(BUTTON_CLOSE_SELECTOR).click(function () {
                hideForm();
            });
            return fHeader;
        };

        var createFormContent = function () {
            return tmpl("formContent");
        };

        var createFormFooter = function () {
            return tmpl("formFooter");
        };

        var setCaption = function (caption) {
            if (isEmpty(caption)) {
                throw "Odin: Caption is not set";
            }
            find(header, CAPTION_SELECTOR).text(caption);
        };

        var addFormClass = function (clazz) {
            if (isEmpty(clazz)) {
                throw "Odin: Additional class is not set";
            }
            form.addClass(clazz);
        };

        var isModified = function () {
            return form.data(DATA_CHANGED_ATTR);
        };

        var setFormSubmit = function (callBack) {
            if (isEmpty(callBack)) {
                throw "Odin: Callback is not set";
            }
            form.submit(function () {
                callBack();
                return false;
            });
        };

        var showForm = function () {
            form.show(0, function () {
                centerDiv(find(form, PANEL_SELECTOR));
            });
            if (type === FormTypesEnum.FORM) {
                find(form, FORM_INPUTS_SELECTOR).change(function () {
                    form.data(DATA_CHANGED_ATTR, true);
                });
            }
        };

        var hideForm = function () {
            var callBack = function () {
                form.hide();
                form.remove();
            };
            callBack();
        };

        this._getHeader = function () {
            return header;
        };

        this._getContent = function () {
            return content;
        };

        this._getFooter = function () {
            return footer;
        };

        this._setCaption = function (caption) {
            setCaption(caption);
        };

        this._setFormSubmit = function (callBack) {
            setFormSubmit(callBack);
        };

        this._addFormClass = function (clazz) {
            addFormClass(clazz);
        };

        this._isModified = function () {
            return isModified();
        };

        this._show = function () {
            showForm();
        };

        this._hide = function () {
            hideForm();
        };

        createForm();
    }

    function OdinToolbar(tableDiv, createCallBack, editCallBack, deleteCallBack) {
        var SELECTOR_ODIN_TABLE = ".odin-table";

        var toolbar;

        var createButton;
        var editButton;
        var deleteButton;

        var initialize = function () {
            if (isEmpty(tableDiv)) {
                throw "Odin-Toolbar: Target div is not set";
            }
            var table = find(tableDiv, SELECTOR_ODIN_TABLE);
            toolbar = tmpl("toolbar");
            toolbar.insertBefore(table, null);
            if (!isEmpty(createCallBack)) {
                createButton = tmpl("createButton", L10nEnum.BUTTON_CREATE);
                createButton.click(function () {
                    createCallBack();
                });
                toolbar.append(createButton);
            }
            if (!isEmpty(editCallBack)) {
                editButton = tmpl("editButton", L10nEnum.BUTTON_EDIT);
                editButton.click(function () {
                    editCallBack();
                });
                setEditButtonState(false);
                toolbar.append(editButton);
            }
            if (!isEmpty(deleteCallBack)) {
                deleteButton = tmpl("deleteButton", L10nEnum.BUTTON_DELETE);
                deleteButton.click(function () {
                    deleteCallBack();
                });
                setDeleteButtonState(false);
                toolbar.append(deleteButton);
            }
        };

        var setEditButtonState = function (state) {
            if (isEmpty(editButton)) {
                return;
            }
            editButton.attr("disabled", !state);
        };

        var setDeleteButtonState = function (state) {
            if (isEmpty(deleteButton)) {
                return;
            }
            deleteButton.attr("disabled", !state);
        };

        this.addButton = function (caption, callBack) {
            if (isEmpty(caption) || isEmpty(callBack)) {
                throw "Odin-Toolbar: Caption or callback is not set";
            }
            var button = tmpl("basicButton", caption);
            button.click(function () {
                callBack();
            });
            toolbar.append(button);
        };

        this.setEditButtonState = function (state) {
            setEditButtonState(state);
        };

        this.setDeleteButtonState = function (state) {
            setDeleteButtonState(state);
        };

        initialize();
    }

    function OdinPaginator(tableDiv, callBack) {
        var SELECTOR_PAGINATOR_CONTENT = ".odin-paginator-content";
        var SELECTOR_PAGINATOR_BUTTON = "a";
        var PAGINATOR_BUTTON_PREFIX = "odin-paginator-";
        var PAGINATOR_SELECTED_BUTTON_CLASS = "active";

        var initialize = function () {
            if (isEmpty(tableDiv)) {
                throw "Odin-Paginator: Target div is not set";
            }
            if (isEmpty(callBack)) {
                throw "Odin-Paginator: Callback is not set";
            }
            tableDiv.append(tmpl("paginator"));
        };

        var drawPaginator = function (data, offset) {
            var paginatorContentDiv = find(tableDiv, SELECTOR_PAGINATOR_CONTENT);
            paginatorContentDiv.empty();
            if (isEmpty(data)) {
                return;
            }
            $.each(data, function (index, value) {
                if (value.name) {
                    paginatorContentDiv.append(tmpl("paginatorButton", value.offset, value.name));
                } else {
                    paginatorContentDiv.append(tmpl("paginatorSeparator"));
                }
            });
            var buttons = find(paginatorContentDiv, SELECTOR_PAGINATOR_BUTTON);
            buttons.removeClass("active");
            buttons.click(function () {
                $('#preloader').show();
                var offset = $(this).attr(ID_ATTR).replace(PAGINATOR_BUTTON_PREFIX, "");
                callBack(offset);
            });
            find(paginatorContentDiv, "#" + PAGINATOR_BUTTON_PREFIX + offset)
                .addClass(PAGINATOR_SELECTED_BUTTON_CLASS);
        };

        this.drawPaginator = function (offset, count) {
            var currentOffset = parseInt(offset ? offset : "0");
            var buttonCount = Math.floor(
                count <= PAGINATOR_ITEMS_PER_PAGE ? 0 : count / PAGINATOR_ITEMS_PER_PAGE) +
                (count < PAGINATOR_ITEMS_PER_PAGE ||
                count % PAGINATOR_ITEMS_PER_PAGE === 0 ? 0 : 1);
            var data = [];
            Array.apply(0, Array(buttonCount)).forEach(function (element, index) {
                if (index > 0 && index < buttonCount - 1 &&
                    buttonCount > PAGINATOR_VISIBLE_BUTTONS_COUNT * 2) {
                    if (index === currentOffset - PAGINATOR_VISIBLE_BUTTONS_COUNT ||
                        index === currentOffset + PAGINATOR_VISIBLE_BUTTONS_COUNT + 1) {
                        data.push({
                            "offset": null,
                            "name": null
                        });
                    }
                    if (index <= currentOffset - PAGINATOR_VISIBLE_BUTTONS_COUNT ||
                        index > currentOffset + PAGINATOR_VISIBLE_BUTTONS_COUNT) {
                        return true;
                    }
                }
                data.push({
                    "offset": index,
                    "name": index + 1
                });
            });
            drawPaginator(data, currentOffset);
        };

        initialize();
    }

    function OdinTableFormatter() {
        var BOOLEAN_TRUE_CLASS = "fa-check";
        var BOOLEAN_FALSE_CLASS = "fa-times";

        var formatBooleanValue = function (value) {
            return tmpl("tableBoolean", (value ? BOOLEAN_TRUE_CLASS : BOOLEAN_FALSE_CLASS));
        };

        var formatDateValue = function (value, dateType) {
            var date = new Date(value);
            var formatterSettings = {};
            switch (dateType) {
                case DateTypesEnum.DATETIME:
                    formatterSettings = {
                        year: "numeric",
                        month: "numeric",
                        day: "numeric",
                        hour: "numeric",
                        minute: "numeric",
                        second: "numeric"
                    };
                    break;
                case DateTypesEnum.TIME:
                    formatterSettings = {
                        hour: "numeric",
                        minute: "numeric",
                        second: "numeric"
                    };
                    break;
            }
            var dateFormatter = new Intl.DateTimeFormat(uiLocale, formatterSettings);
            return dateFormatter.format(date);
        };

        var formatNumericValue = function (value, positiveOnly, scale) {
            var numberFormatter = new Intl.NumberFormat(uiLocale, {
                useGrouping: false,
                minimumFractionDigits: scale
            });
            var formattedValue = numberFormatter.format(value);
            if (positiveOnly && value < 0) {
                return tmpl("tableNumericNegative", formattedValue);
            }
            return formattedValue;
        };

        var formatStringValue = function (value, maxLength, stringType) {
            switch (stringType) {
                case StringTypesEnum.HREF:
                    return tmpl("anchor", value, L10nEnum.GO_TO);
                default:
                    if (isEmpty(maxLength) || maxLength === 0) {
                        return value;
                    }
                    return value.substring(0, Math.min(value.length, maxLength));
            }
        };

        this.formatValue = function (value, fieldParams) {
            if (isEmpty(value) || isEmpty(fieldParams)) {
                return "";
            }
            if (fieldParams.hidden) {
                return value;
            }
            switch (fieldParams.fieldType) {
                case FieldTypesEnum.BOOLEAN:
                    return formatBooleanValue(value);
                case FieldTypesEnum.DATE:
                    return formatDateValue(value, fieldParams.type);
                case FieldTypesEnum.NUMERIC:
                    return formatNumericValue(value, fieldParams.positiveOnly, fieldParams.scale);
                case FieldTypesEnum.STRING:
                    return formatStringValue(value, fieldParams.maxLength, fieldParams.type);
                case FieldTypesEnum.COLLECTION:
                    return L10nEnum.COLLECTION;
                case FieldTypesEnum.OBJECT:
                    return value.view;
                default:
                    return value;
            }
        };
    }

    function OdinTableView(odinDiv, editCallback, globalSelectCallback) {
        var TABLEVIEW_SELECTED_LINE_CLASS = "odin-table-selected-line";
        var TABLEVIEW_LINE_SELECTOR = "tr";
        var TABLEVIEW_SELECTED_LINE_SELECTOR = TABLEVIEW_LINE_SELECTOR +
            "." + TABLEVIEW_SELECTED_LINE_CLASS;
        var TABLEVIEW_LINE_DATA_SELECTOR = TABLEVIEW_LINE_SELECTOR +
            "[" + DATA_ATTR + "=\"{0}\"]";
        var TABLEVIEW_CHOOSABLE_CLASS = "table-hover odin-unselectable";
        var TABLEVIEW_LINE_POINTED_CLASS = "odin-table-pointed-line";
        var TABLEVIEW_HEAD_COLUMN_SELECTOR = "th:nth-child({0})";
        var TABLEVIEW_BODY_COLUMN_SELECTOR = "td:nth-child({0})";

        var tTable;
        var tHead;
        var tBody;
        var choosable;
        var formatter;
        var toolbar;

        var createLine = function (idValue) {
            var prevent = false;
            var timer = 0;
            var newLine = tmpl("tableLine");
            if (!isEmpty(idValue) && choosable) {
                newLine.attr(DATA_ATTR, idValue);
                newLine.click(function () {
                    var btn = $(this);
                    timer = setTimeout(function () {
                        if (!prevent) {
                            btn.toggleClass(TABLEVIEW_SELECTED_LINE_CLASS);
                        }
                        prevent = false;
                        if (!isEmpty(globalSelectCallback)) {
                            globalSelectCallback(getLines());
                        }
                        if (!isEmpty(toolbar)) {
                            toolbar.setEditButtonState(!isEmpty(getLines()));
                            toolbar.setDeleteButtonState(!isEmpty(getLines()));
                        }
                    }, CLICK_DELAY);
                });
                if (!isEmpty(editCallback)) {
                    newLine.dblclick(function () {
                        clearTimeout(timer);
                        prevent = true;
                        editCallback(idValue);
                    });
                }
                newLine.addClass(TABLEVIEW_LINE_POINTED_CLASS);
            }
            return newLine;
        };

        var addLineToHead = function (line) {
            tHead.append(line);
        };

        var addLineToBody = function (line) {
            tBody.append(line);
        };

        var clearBody = function () {
            tBody.empty();
        };

        var initialize = function () {
            if (isEmpty(odinDiv)) {
                throw "Odin: Odin div is not set";
            }
            odinDiv.append(tmpl("table"));
            tTable = find(odinDiv, "table");
            tHead = find(odinDiv, "thead");
            tBody = find(odinDiv, "tbody");
            formatter = new OdinTableFormatter();
        };

        var getLines = function (isAll) {
            var selector = isAll ? TABLEVIEW_LINE_SELECTOR : TABLEVIEW_SELECTED_LINE_SELECTOR;
            var selectedItems = [];
            try {
                find(tBody, selector).each(function () {
                    selectedItems.push({
                        id: $(this).attr(DATA_ATTR),
                        view: $(this).attr(DATA_VIEW_ATTR)
                    });
                });
            } catch (e) {
                // Ignore
            }
            return selectedItems;
        };

        var disableToolbar = function () {
            if (isEmpty(toolbar)) {
                return;
            }
            toolbar.setEditButtonState(false);
            toolbar.setDeleteButtonState(false);
        };

        this.createHead = function (metaData, isOdinDto) {
            if (isEmpty(metaData)) {
                throw "Odin: Metadata is not set";
            }
            choosable = isOdinDto;
            if (choosable) {
                tTable.addClass(TABLEVIEW_CHOOSABLE_CLASS);
            }
            var tHeadLine = createLine();
            $.each(metaData, function (index, value) {
                tHeadLine.append(tmpl("tableHeadColumn", value.caption));
            });
            addLineToHead(tHeadLine);
            $.each(metaData, function (columnIndex, columnParams) {
                if (columnParams.visible === VisibleTypesEnum.NONE) {
                    var index = columnIndex + 1;
                    find(tHead, format(TABLEVIEW_HEAD_COLUMN_SELECTOR, [index])).hide();
                }
            });
        };

        this.fillBody = function (metaData, data) {
            clearBody();
            $.each(data, function (index, value) {
                var idValue = choosable ? value.id : null;
                var line = createLine(idValue);
                line.attr(DATA_VIEW_ATTR, value.view);
                $.each(metaData, function (columnIndex, columnParams) {
                    var column = tmpl("tableBodyColumn");
                    column.append(
                        formatter.formatValue(value[columnParams.fieldName],
                            columnParams));
                    line.append(column);
                });
                addLineToBody(line);
            });
            if (!isEmpty(data)) {
                $.each(metaData, function (columnIndex, columnParams) {
                    if (columnParams.visible === VisibleTypesEnum.NONE) {
                        var index = columnIndex + 1;
                        find(tBody, format(TABLEVIEW_BODY_COLUMN_SELECTOR, [index])).hide();
                    }
                });
            }
        };

        this.getSelectedItems = function () {
            return getLines(false);
        };

        this.getItems = function () {
            return getLines(true);
        };

        // Delete form view only!
        this.deleteById = function (id) {
            if (isEmpty(id)) {
                return;
            }
            try {
                find(tBody, format(TABLEVIEW_LINE_DATA_SELECTOR, [id])).remove();
                disableToolbar();
            } catch (e) {
                // Ignore
            }
        };

        this.setToolbar = function (toolbarVar) {
            toolbar = toolbarVar;
        };

        this.disableToolbar = function () {
            disableToolbar();
        };

        initialize();
    }

    function OdinTableMain(divId, listMetaDataUrl, elementMetaDataUrl, dataUrl, args, disableCopy, globalSaveCallback, globalSelectCallback) {
        if (isEmpty(divId) || isEmpty(listMetaDataUrl) || isEmpty(dataUrl)) {
            throw "Odin: Not enough parameters";
        }

        var OFFSET_ARG = "&offset=";
        var COUNT_ARG = "&count=";

        var odinDiv;
        var tableView;
        var paginator;

        var currentOffset;

        var listMetaData;
        var elementMetaData;

        var params = isEmpty(args) ? "" : args;

        var drawBody = function (offset) {
            if (isEmpty(listMetaData)) {
                throw "Odin: Meta data is not defined";
            }
            currentOffset = isEmpty(offset) ? 0 : parseInt(offset);
            getFromRestWithParams(dataUrl,
                OFFSET_ARG + (currentOffset * PAGINATOR_ITEMS_PER_PAGE) +
                COUNT_ARG + PAGINATOR_ITEMS_PER_PAGE + params, function (data) {
                    if (isEmpty(data)) {
                        throw "Odin: Response data is null";
                    }
                    tableView.fillBody(listMetaData, data.items);
                    if (data.count > 0) {
                        paginator.drawPaginator(offset, data.count);
                    }
                    tableView.disableToolbar();
                    $('#preloader').hide();
                });
        };

        var initialize = function () {
            odinDiv = find($(document.body), "#" + divId);
            tableView = new OdinTableView(odinDiv, editItem, globalSelectCallback);
            paginator = new OdinPaginator(odinDiv, drawBody);
            getFromRest(listMetaDataUrl, function (data) {
                if (isEmpty(data) || isEmpty(data.fields)) {
                    throw "Odin: Can't load list meta data";
                }
                var isOdinDto = data.odinDto;
                listMetaData = data.fields;
                if (isOdinDto && !isEmpty(elementMetaDataUrl)) {
                    getFromRest(elementMetaDataUrl, function (data) {
                        if (isEmpty(data) || isEmpty(data.fields)) {
                            throw "Odin: Can't load element meta data";
                        }
                        elementMetaData = data.fields;
                        var toolbar = new OdinToolbar(odinDiv, createItem, editItem, deleteItem, globalSaveCallback);
                        tableView.setToolbar(toolbar);
                        if (!disableCopy) {
                            toolbar.addButton(L10nEnum.BUTTON_COPY, function () {
                                new OdinCopyBox(dataUrl, drawBody);
                            });
                        }
                    });
                }
                tableView.createHead(listMetaData, isOdinDto);
                drawBody();
            });
        };

        var updateTable = function () {
            // TODO: recalculate offset before redraw table
            drawBody(currentOffset);
        };

        var createItem = function () {
            new OdinForm(elementMetaData, dataUrl, updateTable, null, params);
        };

        var editItem = function (value) {
            if (isEmpty(elementMetaDataUrl)) {
                return;
            }
            var curValue = value;
            if (isEmpty(value)) {
                var selectedItems = tableView.getSelectedItems();
                if (!isEmpty(selectedItems)) {
                    curValue = selectedItems[0].id;
                }
            }
            if (isEmpty(curValue)) {
                return;
            }
            new OdinForm(elementMetaData, dataUrl, updateTable, curValue, params);
        };

        var deleteItem = function () {
            var selectedItems = tableView.getSelectedItems();
            if (isEmpty(selectedItems)) {
                return;
            }
            var callBack = function () {
                $.each(selectedItems, function (index, value) {
                    deleteFromRest(dataUrl + "/" + value.id, null, function () {
                        if (index === selectedItems.length - 1) {
                            updateTable();
                        }
                    });
                });
            };
            new OdinConfirmBox(callBack, L10nEnum.QUESTION_DELETE_ITEM);
        };

        this.getSelectedItems = function () {
            return tableView.getSelectedItems();
        };

        initialize();
    }

    function OdinFormControlGenerator() {
        this.createInputGroup = function () {
            return tmpl("formInputGroup");
        };

        this.createLabel = function (id, caption) {
            return tmpl("formLabel", id, caption);
        };

        var createInput = function (id, value, type) {
            if (isEmpty(id)) {
                throw "Odin: Input id is not set";
            }
            if (isEmpty(type)) {
                throw "Odin: Input type is not set";
            }
            var curValue = isEmpty(value) ? "" : value;
            var wrapper = tmpl("formInputWrapper");
            if (type === InputTypesEnum.CHECKBOX) {
                wrapper.append(tmpl("formInputCheckbox",
                    id, (curValue ? CHECKED_ATTR : NOT_CHECKED_ATTR)));
            } else {
                wrapper.append(tmpl("formInput",
                    id, curValue, type));
            }
            return wrapper;
        };

        this.createBooleanInput = function (id, value) {
            return createInput(id, value, InputTypesEnum.CHECKBOX);
        };

        var fixDateUnit = function (dateUnitValue) {
            return dateUnitValue < 10 ? "0" + dateUnitValue : dateUnitValue;
        };

        var dateToStr = function (value, type) {
            if (isEmpty(value)) {
                return "";
            }
            var date = new Date(value);
            var year = fixDateUnit(date.getFullYear());
            var month = fixDateUnit(date.getMonth() + 1);
            var day = fixDateUnit(date.getDate());
            var hour = fixDateUnit(date.getHours());
            var minute = fixDateUnit(date.getMinutes());
            var formattedDate;
            switch (type) {
                case DateTypesEnum.DATETIME:
                    formattedDate = year + "-" + month + "-" + day + "T" + hour + ":" + minute;
                    break;
                case DateTypesEnum.TIME:
                    formattedDate = hour + ":" + minute;
                    break;
                default:
                    formattedDate = year + "-" + month + "-" + day;
            }
            return formattedDate;
        };

        this.createDateInput = function (id, value, type) {
            var dateType;
            switch (type) {
                case DateTypesEnum.DATETIME:
                    dateType = InputTypesEnum.DATETIME;
                    break;
                case DateTypesEnum.TIME:
                    dateType = InputTypesEnum.TIME;
                    break;
                default:
                    dateType = InputTypesEnum.DATE;
            }
            return createInput(id, dateToStr(value, type), dateType);
        };

        this.createNumericInput = function (id, value) {
            return createInput(id, value, InputTypesEnum.NUMBER);
        };

        this.createStringInput = function (id, value, type) {
            var stringType;
            switch (type) {
                case StringTypesEnum.PASSWORD:
                    stringType = InputTypesEnum.PASSWORD;
                    break;
                case StringTypesEnum.EMAIL:
                    stringType = InputTypesEnum.EMAIL;
                    break;
                default:
                    stringType = InputTypesEnum.TEXT;
            }
            return createInput(id, value, stringType);
        };

        this.dateToStr = function (value, type) {
            return dateToStr(value, type);
        };
    }

    function OdinFormContentGenerator(createNavTabCallBack, isCreateOperation) {
        var PRECISION_CHAR = "9";
        var SCALE_PREFIX = "0.";
        var SCALE_CHAR = "0";
        var SCALE_POSTFIX = "1";
        var OBJECT_INPUT_CLASS = "input-group odin-input-group";
        var CREATE_BUTTON_TEXT_SELECTOR = ".btn-success > span";
        var CHOOSE_BTN_SELECTOR = ".odin-choose-btn";
        var CLEAR_BTN_SELECTOR = ".odin-clear-btn";
        var CLEARABLE_INPUT_SELECTOR = ".form-control";

        var controlGenerator = new OdinFormControlGenerator();

        var createBooleanControl = function (id, caption, value) {
            var group = controlGenerator.createInputGroup();
            group.append(controlGenerator.createLabel(id, caption));
            group.append(controlGenerator.createBooleanInput(id, value));
            return group;
        };

        var createDateControl = function (id, caption, value, type) {
            var group = controlGenerator.createInputGroup();
            group.append(controlGenerator.createLabel(id, caption));
            group.append(controlGenerator.createDateInput(id, value, type));
            return group;
        };

        var createNumericControl = function (id, caption, value, positiveOnly, precision, scale) {
            var group = controlGenerator.createInputGroup();
            group.append(controlGenerator.createLabel(id, caption));
            var control = controlGenerator.createNumericInput(id, value);
            find(control, INPUTS_SELECTOR).each(function () {
                var currentControl = $(this);
                if (positiveOnly) {
                    currentControl.attr(NumberAttrEnum.MIN, 0);
                }
                if (precision) {
                    currentControl
                        .attr(NumberAttrEnum.MAX, Array(precision + 1).join(PRECISION_CHAR));
                }
                if (scale) {
                    var step = SCALE_PREFIX + Array(scale).join(SCALE_CHAR) + SCALE_POSTFIX;
                    currentControl.attr(NumberAttrEnum.STEP, step);
                }
            });
            group.append(control);
            return group;
        };

        var createStringControl = function (id, caption, value, minLength, maxLength, type) {
            var group = controlGenerator.createInputGroup();
            group.append(controlGenerator.createLabel(id, caption));
            var control = controlGenerator.createStringInput(id, value, type);
            find(control, INPUTS_SELECTOR).each(function () {
                var currentControl = $(this);
                currentControl.attr(StringAttrEnum.MIN, minLength);
                currentControl.attr(StringAttrEnum.MAX, maxLength);
            });
            group.append(control);
            return group;
        };

        var createCollectionControl = function (id, caption, values, path) {
            // TODO: add required check if needed
            var content = $(tmpl("emptyDiv"));
            var tabView = new OdinTableView(content, null, null);
            var createAction = function () {
                new OdinChooseBox(path, function (selectedItems) {
                    if (isEmpty(selectedItems)) {
                        return;
                    }
                    var currentValues = tabView.getItems();
                    $.merge(currentValues, selectedItems);
                    var existingIDs = [];
                    currentValues = $.grep(currentValues, function (v) {
                        if ($.inArray(v.id, existingIDs) !== -1) {
                            return false;
                        } else {
                            existingIDs.push(v.id);
                            return true;
                        }
                    });
                    tabView.fillBody(metaData, currentValues);
                });
            };
            var deleteAction = function () {
                var callBack = function () {
                    $.each(tabView.getSelectedItems(), function (index, value) {
                        tabView.deleteById(value.id);
                    });
                };
                new OdinConfirmBox(callBack, L10nEnum.QUESTION_DELETE_ITEM);
            };
            content.append(tabView);
            var metaData = [
                {
                    "fieldType": "string",
                    "fieldName": "id",
                    "caption": "id",
                    "visible": "none"
                },
                {
                    "fieldType": "string",
                    "fieldName": "view",
                    "caption": L10nEnum.VIEW,
                    "visible": "all"
                }
            ];
            tabView.createHead(metaData, true);
            tabView.fillBody(metaData, values);
            createNavTabCallBack(id, caption, content);
            var toolbar = new OdinToolbar(content, createAction, null, deleteAction);
            tabView.setToolbar(toolbar);
            find(content, CREATE_BUTTON_TEXT_SELECTOR).text(L10nEnum.BUTTON_ADD);
            return null;
        };

        var createObjectControl = function (id, caption, value, path) {
            // TODO: add required check if needed
            var chooseInput = controlGenerator.createStringInput(
                id, isEmpty(value) ? "" : value.view, StringTypesEnum.TEXT);
            chooseInput.addClass(OBJECT_INPUT_CLASS);
            var buttonGroup = tmpl("buttonGroup");
            chooseInput.append(buttonGroup);
            var chooseButton = tmpl("chooseButton");
            buttonGroup.append(chooseButton);
            find(buttonGroup, CHOOSE_BTN_SELECTOR).click(function () {
                new OdinChooseBox(path, function (selectedItems) {
                    if (isEmpty(selectedItems)) {
                        return;
                    }
                    find(chooseInput, CLEARABLE_INPUT_SELECTOR).each(function () {
                        $(this)
                            .val(selectedItems[0].view)
                            .attr(DATA_ATTR, selectedItems[0].id);
                    });
                });
            });
            var clearButton = tmpl("clearButton");
            buttonGroup.append(clearButton);
            find(buttonGroup, CLEAR_BTN_SELECTOR).click(function () {
                find(chooseInput, CLEARABLE_INPUT_SELECTOR).val("");
            });
            find(chooseInput, CLEARABLE_INPUT_SELECTOR).each(function () {
                $(this)
                    .attr(DISABLED_ATTR, true)
                    .attr(DATA_ATTR, isEmpty(value) ? "" : value.id);
            });
            var group = controlGenerator.createInputGroup();
            group.append(controlGenerator.createLabel(id, caption));
            group.append(chooseInput);
            return group;
        };

        var configureControl = function (control, hidden, readonly, notempty) {
            if (isEmpty(control)) {
                return;
            }
            find(control, LABELS_SELECTOR).each(function () {
                var currentControl = $(this);
                if (hidden) {
                    currentControl.hide();
                }
            });
            find(control, INPUTS_SELECTOR).each(function () {
                var currentControl = $(this);
                if (hidden) {
                    currentControl.hide();
                    currentControl.attr(TYPE_ATTR, InputTypesEnum.HIDDEN);
                }
                if (readonly) {
                    currentControl.attr(DISABLED_ATTR, true);
                }
                if (notempty) {
                    currentControl.attr(REQUIRED_ATTR, true);
                }
            });
            return control;
        };

        this.createFormControl = function (value, fieldParams) {
            if (isEmpty(fieldParams)) {
                return null;
            }
            var control;
            switch (fieldParams.fieldType) {
                case FieldTypesEnum.BOOLEAN:
                    control = createBooleanControl(fieldParams.fieldName,
                        fieldParams.caption, value);
                    break;
                case FieldTypesEnum.DATE:
                    control = createDateControl(fieldParams.fieldName,
                        fieldParams.caption, value, fieldParams.type);
                    break;
                case FieldTypesEnum.NUMERIC:
                    control = createNumericControl(fieldParams.fieldName,
                        fieldParams.caption, value, fieldParams.positiveOnly,
                        fieldParams.precision, fieldParams.scale);
                    break;
                case FieldTypesEnum.STRING:
                    control = createStringControl(fieldParams.fieldName,
                        fieldParams.caption, value, fieldParams.minLength,
                        fieldParams.maxLength, fieldParams.type);
                    break;
                case FieldTypesEnum.COLLECTION:
                    control = createCollectionControl(fieldParams.fieldName,
                        fieldParams.caption, value, fieldParams.path);
                    break;
                case FieldTypesEnum.OBJECT:
                    control = createObjectControl(fieldParams.fieldName,
                        fieldParams.caption, value, fieldParams.path);
                    break;
                default:
                    return null;
            }
            var hidden;
            switch (fieldParams.visible) {
                case VisibleTypesEnum.NONE:
                    hidden = true;
                    break;
                case VisibleTypesEnum.ON_CREATE:
                case VisibleTypesEnum.ON_UPDATE:
                    hidden = isCreateOperation;
                    break;
                default:
                    hidden = false;
            }
            return configureControl(control, hidden, fieldParams.readOnly, fieldParams.notEmpty);
        };

        this.getCurrentDateStr = function () {
            return controlGenerator.dateToStr(new Date(), DateTypesEnum.DATE);
        };
    }

    function OdinForm(metaData, dataUrl, tableCallBack, id, args) {
        if (isEmpty(metaData) || isEmpty(dataUrl) || isEmpty(tableCallBack)) {
            throw "Odin: Not enough parameters";
        }

        var DEFAULT_TAB_SELECTOR = "odin-values";
        var TABS_HEADER_SELECTOR = ".nav-tabs";
        var TABS_CONTENT_SELECTOR = ".tab-content";
        var TAB_SELECTOR = ".tab-pane";
        var COLLECTION_ITEM_SELECTOR = "tbody > tr";

        OdinFormCommon.call(this, FormTypesEnum.FORM);

        var self = this;

        var generator;

        var params = isEmpty(args) ? "" : args;

        var isCreateForm = function () {
            return isEmpty(id);
        };

        var formToJson = function () {
            var formData = {};
            try {
                findFiltered(self._getContent(), TAB_SELECTOR, "#" + DEFAULT_TAB_SELECTOR)
                    .each(function () {
                        var tabData = [];
                        try {
                            find($(this), COLLECTION_ITEM_SELECTOR)
                                .each(function () {
                                    if ($(this).attr(DATA_ATTR)) {
                                        tabData.push({
                                            id: $(this).attr(DATA_ATTR),
                                            view: $(this).attr(DATA_VIEW_ATTR)
                                        });
                                        return true;
                                    }
                                });
                        } catch (e) {
                            // Ignore
                        }
                        formData[$(this).attr(ID_ATTR)] = tabData;
                    });
            } catch (e) {
                // Ignore
            }
            findFiltered(self._getContent(), INPUTS_SELECTOR, BUTTONS_SELECTOR)
                .each(function () {
                    if (isEmpty($(this).val())) {
                        return true;
                    }
                    if ($(this).attr(DATA_ATTR)) {
                        formData[$(this).attr(ID_ATTR)] = {
                            id: $(this).attr(DATA_ATTR),
                            view: $(this).val()
                        };
                        return true;
                    }
                    switch ($(this).attr(TYPE_ATTR)) {
                        case InputTypesEnum.CHECKBOX:
                            formData[$(this).attr(ID_ATTR)] = $(this).is(":" + CHECKED_ATTR);
                            break;
                        case InputTypesEnum.DATE:
                        case InputTypesEnum.DATETIME:
                            formData[$(this).attr(ID_ATTR)] = isEmpty($(this).val) ?
                                "" : Date.parse($(this).val());
                            break;
                        case InputTypesEnum.TIME:
                            formData[$(this).attr(ID_ATTR)] = isEmpty($(this).val) ?
                                "" : Date.parse(generator.getCurrentDateStr() +
                                    "T" + $(this).val());
                            break;
                        case InputTypesEnum.NUMBER:
                            formData[$(this).attr(ID_ATTR)] = parseFloat($(this).val());
                            break;
                        default:
                            formData[$(this).attr(ID_ATTR)] = $(this).val();
                    }
                });
            return JSON.stringify(formData);
        };

        var initialize = function () {
            generator = new OdinFormContentGenerator(createNavTab, isCreateForm());
            self._getContent()
                .append(tmpl("formTabs", DEFAULT_TAB_SELECTOR, L10nEnum.CAPTION_DEFAULT_TAB));
            var okBtn = tmpl("okButton", L10nEnum.BUTTON_SAVE);
            okBtn.attr(TYPE_ATTR, InputTypesEnum.SUBMIT);
            var closeBtn = tmpl("cancelButton", L10nEnum.BUTTON_CLOSE);
            self._getFooter().append(closeBtn);
            self._getFooter().append(okBtn);
            if (isCreateForm()) {
                configureAndShowForm();
            } else {
                getFromRest(dataUrl + "/" + id, function (data) {
                    if (isEmpty(data)) {
                        throw format("Odin: Can't load data by id {0} and url {1}", [id, dataUrl]);
                    }
                    configureAndShowForm(data);
                });
            }
            closeBtn.click(function () {
                closeForm();
            });
        };

        var configureAndShowForm = function (data) {
            var saveCallBack = function () {
                saveData(formToJson());
            };
            self._setCaption(isEmpty(data) ?
                L10nEnum.CAPTION_CREATE_FORM :
                data.view + " " + L10nEnum.CAPTION_EDIT_FORM);
            self._setFormSubmit(saveCallBack);
            $.each(metaData, function (fieldIndex, fieldParams) {
                var value = isEmpty(data) ?
                    null : data[fieldParams.fieldName];
                find(self._getContent(), "#" + DEFAULT_TAB_SELECTOR)
                    .append(generator.createFormControl(value, fieldParams));
            });
            self._show();
        };

        var createNavTab = function (id, caption, content) {
            find(self._getContent(), TABS_HEADER_SELECTOR).append(tmpl("formTab", id, caption));
            var tab = tmpl("formTabContent", id);
            tab.append(content);
            find(self._getContent(), TABS_CONTENT_SELECTOR).append(tab);
        };

        var saveData = function (data) {
            var saveCallback = function (responseData) {
                if (isEmpty(responseData)) {
                    throw "Odin: Error on data save";
                }
                self._hide();
                tableCallBack();
                if (!isEmpty(globalSaveCallback)) {
                    globalSaveCallback();
                }
            };
            if (isCreateForm()) {
                postToRestWithParams(dataUrl, data, params, saveCallback);
            } else {
                putToRestWithParams(dataUrl, data, params, saveCallback);
            }
        };

        var closeForm = function () {
            if (self._isModified()) {
                new OdinConfirmBox(self._hide, L10nEnum.QUESTION_LOST_CHANGES);
                return;
            }
            self._hide();
        };

        initialize();
    }

    function OdinConfirmBox(callBack, questionText) {
        if (isEmpty(callBack) || isEmpty(questionText)) {
            throw "Odin: Not enough parameters";
        }

        OdinFormCommon.call(this, FormTypesEnum.CONFIRM);

        var self = this;

        var initialize = function () {
            var yesBtn = tmpl("okButton", L10nEnum.BUTTON_OK);
            var cancelBtn = tmpl("cancelButton", L10nEnum.BUTTON_CANCEL);
            self._setCaption(L10nEnum.QUESTION_DEFAULT_CAPTION);
            self._getContent().text(questionText);
            self._getFooter().append(yesBtn);
            self._getFooter().append(cancelBtn);
            self._show();

            yesBtn.click(function () {
                self._hide();
                callBack();
                if (!isEmpty(globalSaveCallback)) {
                    globalSaveCallback();
                }
            });
            cancelBtn.click(function () {
                self._hide();
            });
        };

        initialize();
    }

    function OdinChooseBox(path, callBack) {
        if (isEmpty(path) || isEmpty(callBack)) {
            throw "Odin: Not enough parameters";
        }

        var ADDITIONAL_FORM_CLASS = "odin-table-box";
        var ADDITIONAL_CONTENT_CLASS = "odin-kill-padding";
        var CONTENT_ID_VALUE = "table-block";

        OdinFormCommon.call(this);

        var self = this;

        var initialize = function () {
            self._addFormClass(ADDITIONAL_FORM_CLASS);
            self._getContent().addClass(ADDITIONAL_CONTENT_CLASS);
            self._getContent().attr(ID_ATTR, CONTENT_ID_VALUE);
            var yesBtn = tmpl("okButton", L10nEnum.BUTTON_OK);
            var cancelBtn = tmpl("cancelButton", L10nEnum.BUTTON_CANCEL);
            self._getFooter().append(cancelBtn);
            self._getFooter().append(yesBtn);
            var table = new OdinTableMain("table-block", path + odinMetaList, null, path);
            self._show();

            yesBtn.click(function () {
                self._hide();
                callBack(table.getSelectedItems());
            });
            cancelBtn.click(function () {
                self._hide();
            });
        };

        initialize();
    }

    function OdinCopyBox(dataUrl, tableRedrawCallback) {
        if (isEmpty(dataUrl) || isEmpty(tableRedrawCallback)) {
            throw "Odin: Not enough parameters";
        }

        var ADDITIONAL_FORM_CLASS = "odin-simple-form";
        var FROM_VERSION_SELECTOR = "#fromVersion";
        var VERSION_SELECTOR = "#version";

        OdinFormCommon.call(this);

        var self = this;

        var initialize = function () {
            self._setCaption(L10nEnum.CAPTION_COPY_FORM);
            self._addFormClass(ADDITIONAL_FORM_CLASS);
            self._getContent().append(tmpl("copyForm"));
            var from = find(self._getContent(), FROM_VERSION_SELECTOR);
            var to = find(self._getContent(), VERSION_SELECTOR);
            var yesBtn = tmpl("okButton", L10nEnum.BUTTON_OK);
            var cancelBtn = tmpl("cancelButton", L10nEnum.BUTTON_CANCEL);
            self._getFooter().append(cancelBtn);
            self._getFooter().append(yesBtn);

            /*getFromRest(urlVersions, function (versions) {
                if (isEmpty(versions) || versions.count === 0) {
                    return;
                }
                var versionsData = [];
                versions.items.forEach(function (version) {
                    versionsData.push({
                        id: version.id,
                        name: version.name + " от " +
                        new Date(version.date).toLocaleDateString(uiLocale)
                    });
                });
                fillSelect(from, versionsData.sort(function (a, b) {
                    return a.id - b.id;
                }));
                var currentVersion = getCurrentVersionData();
                if (!isEmpty(currentVersion)) {
                    to.attr(DATA_ATTR, currentVersion.id);
                    to.val(currentVersion.name);
                }
                self._show();
            });*/

            yesBtn.click(function () {
                if (isEmpty(from.val()) || isEmpty(to.val())) {
                    return;
                }
                if (from.val() === to.attr(DATA_ATTR)) {
                    showFeedbackMessage(L10nEnum.ERROR_VERSIONS_MATCH, MessageTypesEnum.DANGER);
                    return;
                }
                postToRestWithVersionAndParams(dataUrl + odinCopy, null,
                    "&fromVersionId=" + from.val(), function () {
                        self._hide();
                        tableRedrawCallback();
                    });
            });
            cancelBtn.click(function () {
                self._hide();
            });
        };

        initialize();
    }
}
