Jsw.onReady(function() {
    var severityLevel = {
        INFO: 1,
        ERROR: 2,
        WARNING: 3,
        DEBUG: 4,
        EXCEPTION: 5,
        MIGRATOR_EXECUTION: 6
    };

    var severityProperties = {};
    severityProperties[severityLevel.INFO] = {
        'title': 'Info',
        'rowCssClass': 'plesk-migrator-log-row-severity-info',
        'severityCellCssClass': 'plesk-migrator-log-cell-severity-info'
    };
    severityProperties[severityLevel.ERROR] = {
        'title': 'Error',
        'rowCssClass': 'plesk-migrator-log-row-severity-error',
        'severityCellCssClass': 'plesk-migrator-log-cell-severity-error'
    };
    severityProperties[severityLevel.WARNING] = {
        'title': 'Warning',
        'rowCssClass': 'plesk-migrator-log-row-severity-warning',
        'severityCellCssClass': 'plesk-migrator-log-cell-severity-warning'
    };
    severityProperties[severityLevel.DEBUG] = {
        'title': 'Debug',
        'rowCssClass': 'plesk-migrator-log-row-severity-debug',
        'severityCellCssClass': 'plesk-migrator-log-cell-severity-debug'
    };
    severityProperties[severityLevel.EXCEPTION] = {
        'title': 'Exception',
        'rowCssClass': 'plesk-migrator-log-row-severity-debug',
        'severityCellCssClass': 'plesk-migrator-log-cell-severity-debug'
    };
    severityProperties[severityLevel.MIGRATOR_EXECUTION] = {
        'title': 'Migrator execution',
        'rowCssClass': 'plesk-migrator-log-row-severity-migrator-execution',
        'severityCellCssClass': 'plesk-migrator-log-cell-severity-migrator-execution'
    };


    var LogRecords = Class.create({
        initialize: function() {
            this._records = {};  // recordId => LogRecord
            this._expandedRecords = {};
            this._rootRecords = [];
            this._inProgressRecords = {};
            this._htmlLogTable = $('migration-log-table');
            this._htmlLogTableBody = this._htmlLogTable.select('tbody').first();

            this._showDebug = false;
            this._highlightText = null;
            this._filterText = null;
        },
        appendRecords: function(recordsData) {
            for (var i = 0; i < recordsData.length; i++) {
                var recordData = recordsData[i];
                this.appendRecord(recordData);
            }
        },
        appendRecord: function(recordData) {
            if (this._records[recordData.recordId]) {
                var existingRecord = this._records[recordData.recordId];
                this.updateRecordExpandability(existingRecord);
                if (recordData.finished) {
                    existingRecord.finishRecord();
                    delete this._inProgressRecords[existingRecord.getRecordId()];
                }
                existingRecord.updateRecordChildErrorMark(recordData.hasChildErrors, recordData.hasChildWarnings);
                return;
            }

            var parentRecord = null;
            var rootParentRecordId = 2147483647;
            if (recordData.parentRecordId != rootParentRecordId) {
                parentRecord = this._records[recordData.parentRecordId];
            }

            var record = new LogRecord(this, recordData, parentRecord);
            this._records[record.getRecordId()] = record;

            if (!record.isFinished()) {
                this._inProgressRecords[record.getRecordId()] = record;
            }

            if (parentRecord) {
                parentRecord.appendChildRecord(record);
                parentRecord.getHtmlEndRow().insert({before: record.getHtmlRow()});
                parentRecord.getHtmlEndRow().insert({before: record.getHtmlEndRow()});
                this.updateRecordExpandability(parentRecord);
            } else {
                this._htmlLogTableBody.insert(record.getHtmlRow());
                this._htmlLogTableBody.insert(record.getHtmlEndRow());
                this._rootRecords.push(record);
            }

            record.setCollapsed();
            this.updateRecordExpandability(record);

            record.highlightText(this._highlightText);
            this.updateRecordVisibility(record);
        },
        expandRecord: function(record) {
            this.expandRecordsByIds([record.getRecordId()]);
        },
        expandRecordsByIds: function(recordIds) {
            var self = this;

            var collapsedRecordIds = [];
            for (var i = 0; i < recordIds.length; i++) {
                var recordId = recordIds[i];
                if (self._expandedRecords[recordId] != 1) {
                    collapsedRecordIds.push(recordId);
                }
            }

            if (collapsedRecordIds.length == 0) {
                return
            }

            var requestFunctions = [];

            // divide list of records into parts, to handle long lists of expanded records:
            // when there are too many records in a single AJAX request, backend could
            // fail to respond (because of memory limit on response, or max execution time of PHP or CGI).
            var maxPartSize = 50;
            var collapsedRecordIdsParts = divideIntoParts(collapsedRecordIds, maxPartSize);

            collapsedRecordIdsParts.forEach(function(collapsedRecordIdsPart) {
                requestFunctions.push(function (onSuccess) {
                    new Ajax.Request(URL_GET_LOG_RECORDS, {
                        // perform POST request as list of parent IDs could be long and exceed URL length limit
                        // (the problem is more actual for IIS which has more strict limits)
                        method: 'post',
                        parameters: {
                            'parent-ids': collapsedRecordIdsPart.join()
                        },
                        onSuccess: function (response) {
                            var recordsData = response.responseText.evalJSON();

                            self.appendRecords(recordsData);

                            for (var i = 0; i < collapsedRecordIdsPart.length; i++) {
                                var recordId = collapsedRecordIdsPart[i];
                                self._records[recordId].setExpanded();
                                self._expandedRecords[recordId] = 1;
                                for (var j = 0; j < self._records[recordId].getChildRecords().length; j++) {
                                    var childRecord = self._records[recordId].getChildRecords()[j];
                                    self.updateRecordVisibility(childRecord);
                                }
                            }
                        },
                        onComplete: function() {
                            onSuccess();
                        }
                    });
                });
            });

            callAsyncFunctionsChain(requestFunctions);
        },
        expandRecordsByIdsRaw: function(recordIds) {
            var self = this;

            for (var i = 0; i < recordIds.length; i++) {
                var recordId = recordIds[i];
                if (self._expandedRecords[recordId] == 1) {
                    continue;
                }
                if (!self._records[recordId].isExpandable()) {
                    continue;
                }

                self._records[recordId].setExpanded();
                self._expandedRecords[recordId] = 1;
                for (var j = 0; j < self._records[recordId].getChildRecords().length; j++) {
                    var childRecord = self._records[recordId].getChildRecords()[j];
                    self.updateRecordVisibility(childRecord);
                }
            }
        },
        collapseRecord: function(record) {
            for (var i = 0; i < record.getChildRecords().length; i++) {
                var childRecord = record.getChildRecords()[i];

                for (var j = 0; j < childRecord.getChildRecords().length; j++) {
                    var childChildRecord = childRecord.getChildRecords()[j];
                    this._removeRecord(childChildRecord);
                }
                childRecord.removeAllChildRecords();
                childRecord.hide();
                childRecord.setCollapsed();
                delete this._expandedRecords[childRecord.getRecordId()];
            }
            record.setCollapsed();

            delete this._expandedRecords[record.getRecordId()];
        },
        _removeRecord: function(record) {
            for (var i = 0; i < record.getChildRecords().length; i++) {
                var childRecord = record.getChildRecords()[i];
                this._removeRecord(childRecord);
            }

            delete this._records[record.getRecordId()];
            record.getHtmlRow().remove();
            record.getHtmlEndRow().remove();
        },
        toggleDebug: function() {
            var self = this;
            this._showDebug = !this._showDebug;
            this._foreachRecord(function(record) {
                self.updateRecordVisibility(record);
            });
            this._foreachRecord(function(record) {
                self.updateRecordExpandability(record);
            });
        },
        getExpandedRecordIds: function() {
            return Object.keys(this._expandedRecords);
        },
        getInProgressRecords: function () {
            return this._inProgressRecords;
        },
        getRootRecords: function() {
            return this._rootRecords;
        },
        setHighlightText: function(searchText) {
            var self = this;
            this._highlightText = searchText;
            this._foreachRecord(function(record) {
                record.highlightText(searchText)
            });
            this._foreachRecord(function(record) {
                self.updateRecordExpandability(record);
            });
        },
        setFilterText: function(searchText)  {
            var self = this;
            this._filterText = searchText;
            this._foreachRecord(function(record) {
                self.updateRecordVisibility(record);
            });
            this._foreachRecord(function(record) {
                self.updateRecordExpandability(record);
            });
        },
        updateRecordVisibility: function(record) {
            if (this._isFiltered(record)) {
                record.hide();
                return;
            }
            if (record.getParentRecord() &&!record.getParentRecord().isExpanded()) {
                record.hide();
                return;
            }
            record.show();
        },
        updateRecordExpandability: function(record) {
            for (var i = 0; i < record.getChildRecords().length; i++) {
                var childRecord = record.getChildRecords()[i];
                if (!this._isFiltered(childRecord)) {
                    record.setExpandable(true);
                    return;
                }
            }
            record.setExpandable(false);
        },
        _foreachRecord: function(callback) {
            var self = this;
            Object.keys(this._records).forEach(function(key, index) {
                var record = self._records[key];
                callback(record);
            });
        },
        _isFiltered: function(record) {
            return (
                (record.isDebug() && !this._showDebug) ||
                (
                    this._filterText &&
                    !(record.getMessage().toLowerCase().indexOf(this._filterText.toLowerCase()) > -1)
                )
            );
        }
    });

    var LogRecord = Class.create({
        initialize: function(logRecords, logRecordData, parentRecord) {
            this._logRecords = logRecords;

            this._childRecords = [];

            this._recordId = logRecordData.recordId;
            this._severity = logRecordData.severity;
            this._time = logRecordData.time;
            this._message = logRecordData.message;
            this._finished = logRecordData.finished;

            this._parentRecord = parentRecord;

            this._isExpandable = false;

            this._initializeHtml();
            this.updateRecordChildErrorMark(logRecordData.hasChildErrors, logRecordData.hasChildWarnings);
        },
        _initializeHtml: function() {
            var messageCellProperties = {
                'style': 'white-space: pre-wrap; word-wrap: break-word; padding-left: ' + (this.getLevel() * 50) + 'px',
                'class': 'messageText'
            };

            var severityDescriptor = severityProperties[this._severity];

            var rowProperties = {'class': severityDescriptor.rowCssClass + ' logItemRow', 'style': 'display: none'};
            var severityCellProperties = {
                'class': severityDescriptor.severityCellCssClass,
                'style': 'white-space: nowrap'
            };

            this._htmlRow = new Element('tr', rowProperties);

            this._htmlTimeCol = new Element('td', {'class': 'logRecordDate'}).update(this._time.escapeHTML());
            this._htmlErrorIcon = new Element(
                'img', {
                    'src': migratorImage('log/error.png'),
                    'style': 'width: 16px; height: 16px; margin-left: 8px; display: none;'
                }
            );
            this._htmlWarningIcon = new Element(
                'img', {
                    'src': migratorImage('log/warning.png'),
                    'style': 'width: 16px; height: 16px; margin-left: 8px; display: none;'
                }
            );
            this._htmlSeverityCol = new Element('td', severityCellProperties).update(
                severityDescriptor.title.escapeHTML()
            ).insert(this._htmlErrorIcon).insert(this._htmlWarningIcon);

            this._htmlMessageCol = new Element('td', messageCellProperties);

            var htmlMessageColContainer = new Element('div', {'class': 'plesk-migrator-log-container-message-column'});
            this._htmlWorkingContainer = new Element('div', {'class': 'plesk-migrator-log-working-container'});
            this._htmlExpandContainer = new Element('div', {'class': 'plesk-migrator-log-expand-container'});
            this._htmlMessageContainer = new Element('div', {'class': 'plesk-migrator-log-message-container'});
            this._htmlMessageContainer.update(this._message.escapeHTML());
            htmlMessageColContainer.insert(this._htmlWorkingContainer);
            htmlMessageColContainer.insert(this._htmlExpandContainer);
            htmlMessageColContainer.insert(this._htmlMessageContainer);
            this._htmlMessageCol.insert(htmlMessageColContainer);



            this._htmlExpandIcon = new Element(
                'img', {
                    'src': migratorImage('log-expand-icon.gif'),
                    'class': 'plesk-migrator-expand-image',
                    'style': 'display: none'
                }
            );
            this._htmlCollapseIcon = new Element(
                'img', {
                    'src': migratorImage('log-collapse-icon.gif'),
                    'class': 'plesk-migrator-expand-image',
                    'style': 'display: none'
                }
            );

            if (!this._finished) {
                this._htmlWorkingIcon = new Element(
                    'img', {
                        'src': migratorImage('log-record-working.gif'),
                        'style': 'width: 16px; height: 16px;'
                    }
                );
                this._htmlWorkingContainer.insert(this._htmlWorkingIcon);
            } else {
                this._htmlWorkingIcon = null;
            }

            this._htmlExpandContainer.insert(this._htmlExpandIcon);
            this._htmlExpandContainer.insert(this._htmlCollapseIcon);

            this._htmlRow.insert(this._htmlTimeCol);
            this._htmlRow.insert(this._htmlSeverityCol);
            this._htmlRow.insert(this._htmlMessageCol);

            this._expanded = false;

            this._endHtmlRow = new Element('tr', {'style': 'display: none'});
        },
        appendChildRecord: function(record) {
            this._childRecords.push(record);
        },
        getChildRecords: function() {
            return this._childRecords;
        },
        removeAllChildRecords: function() {
            this._childRecords = [];
        },
        getHtmlRow: function() {
            return this._htmlRow;
        },
        // Get fake hidden HTML row, which marks place right after the last child record.
        // So, DOM tree will look like:
        //   <tr>This log record</tr>
        //   <tr>Child record #1</tr>
        //   <tr>Child record #2</tr>
        //   ...
        //   <tr>Child record #N (last child record)</tr>
        //   <tr>End record (hidden)</tr>
        getHtmlEndRow: function () {
            return this._endHtmlRow;
        },
        getRecordId: function() {
            return this._recordId;
        },
        getMessage: function() {
            return this._message;
        },
        getParentRecord: function() {
            return this._parentRecord;
        },
        getLevel: function() {
            var i = 0;
            var parent = this.getParentRecord();
            while (parent) {
                i++;
                parent = parent.getParentRecord();
            }
            return i;
        },
        hide: function() {
            this.getHtmlRow().hide();
        },
        show: function() {
            this.getHtmlRow().show();
        },
        setExpandable: function(isExpandable) {
            this._isExpandable = isExpandable;
            if (isExpandable) {
                if (this._expanded) {
                    this._htmlCollapseIcon.show();
                    this._htmlExpandIcon.hide();
                } else {
                    this._htmlCollapseIcon.hide();
                    this._htmlExpandIcon.show();
                }
            } else {
                this._htmlCollapseIcon.hide();
                this._htmlExpandIcon.hide();
            }
        },
        isExpandable: function() {
            return this._isExpandable;
        },
        setExpanded: function() {
            var record = this;
            this._htmlCollapseIcon.show();
            this._htmlExpandIcon.hide();
            this._htmlExpandContainer.stopObserving('click');
            this._htmlExpandContainer.observe(
                'click', function() {
                    record._logRecords.collapseRecord(record);
                }
            );
            this._expanded = true;
        },
        setCollapsed: function() {
            var record = this;
            this._htmlCollapseIcon.hide();
            this._htmlExpandIcon.show();
            this._htmlExpandContainer.stopObserving('click');
            this._htmlExpandContainer.observe(
                'click', function () {
                    record._logRecords.expandRecord(record);
                }
            );
            this._expanded = false;
        },
        finishRecord: function() {
            this._finished = true;
            if (this._htmlWorkingIcon) {
                this._htmlWorkingIcon.remove();
                this._htmlWorkingIcon = null;
            }
        },
        updateRecordChildErrorMark: function(hasChildErrors, hasChildWarnings) {
            if (hasChildErrors) {
                this._htmlWarningIcon.hide();
                this._htmlErrorIcon.show();
            } else if (hasChildWarnings) {
                this._htmlWarningIcon.show();
            }
        },
        isFinished: function() {
            return this._finished;
        },
        isExpanded: function() {
            return this._expanded;
        },
        isDebug: function() {
            return this._severity == severityLevel.DEBUG || this._severity == severityLevel.EXCEPTION;
        },
        highlightText: function(searchText) {
            this._htmlMessageContainer.update(highlight(this._message, searchText));
        }
    });

    function divideIntoParts(list, maxPartSize)
    {
        var parts = [];
        for (var i = 0; i < list.length; i+= maxPartSize) {
            parts.push(list.slice(i, i + maxPartSize))
        }
        return parts;
    }

    // Call chain of asynchronous functions, one-by-one: the first function is called, when it completes its
    // async execution, the second function is called, when it completes its async execution, the third function
    // is called, and so on.
    // "functions" argument is a list, each item is a function, which accepts single argument - callback,
    // which must be called by the function once it completes its async execution.
    function callAsyncFunctionsChain(functions)
    {
        if (functions.length == 0) {
            return;
        }

        var functionId = 0;

        function callNextRequestFunction() {
            if (functionId >= functions.length) {
                return;
            }

            functions[functionId](function() {
                functionId++;
                setTimeout(function() {callNextRequestFunction();}, 0);
            });
        }

        callNextRequestFunction();
    }

    // Update log periodically with AJAX
    function periodicUpdateLog() {
        var rootParentRecordId = 2147483647;

        var lastRootRecordId = rootParentRecordId;
        var rootRecords = logRecords.getRootRecords();
        if (rootRecords.length > 0) {
            lastRootRecordId = rootRecords[rootRecords.length - 1].getRecordId();
        }

        var recordIdPairs = [
            rootParentRecordId + '.' + lastRootRecordId
        ];

        var inProgressRecs = logRecords.getInProgressRecords();
        for (var recId in inProgressRecs) {
            if (inProgressRecs.hasOwnProperty(recId)) {
                var inProgressRecord = inProgressRecs[recId];
                var childRecords = inProgressRecord.getChildRecords();
                var lastChildRecordId = rootParentRecordId;
                if (childRecords.length > 0) {
                    lastChildRecordId = childRecords[childRecords.length - 1].getRecordId();
                }

                recordIdPairs.push(inProgressRecord.getRecordId() + "." + lastChildRecordId)
            }
        }

        var url = URL_GET_LOG_UPDATES;
        if ($('automaticExpandBottom').checked) {
            url += '/get-all-new-records/true';
        }

        new Ajax.Request(url, {
            // perform POST request as list of parent IDs could be long and exceed URL length limit
            // (the problem is more actual for IIS which has more strict limits)
            method: 'post',
            parameters: {
                'in-progress-record-ids': recordIdPairs.join(",")
            },
            onSuccess: function (response) {
                var recordsData = response.responseText.evalJSON();
                if (recordsData.length > 0) {
                    logRecords.appendRecords(recordsData);

                    if ($('automaticExpandBottom').checked) {
                        var recordIds = [];
                        recordsData.forEach(function (recordData) {
                            recordIds.push(recordData.recordId);
                        });
                        logRecords.expandRecordsByIdsRaw(recordIds);
                    }

                    if ($('automaticScrollBottom').checked) {
                        scrollToBottom();
                    }
                }
            },
            onComplete: function() {
                setTimeout(function() {periodicUpdateLog()}, 2000);
            }
        });
    }

    function scrollToBottom() {
        window.scrollTo(0, document.body.scrollHeight);
    }

    function synchronizeScrollCheckboxes() {
        $('automaticScrollTop').observe('change', function (e) {
            $('automaticScrollBottom').checked = $('automaticScrollTop').checked;
        });
        $('automaticScrollBottom').observe('change', function (e) {
            $('automaticScrollTop').checked = $('automaticScrollBottom').checked;
        });
    }

    function observeDebugCheckboxes() {
        $('showDebugTop').observe('change', function (e) {
            $('showDebugBottom').checked = $('showDebugTop').checked;
            logRecords.toggleDebug();
        });
        $('showDebugBottom').observe('change', function (e) {
            $('showDebugTop').checked = $('showDebugBottom').checked;
            logRecords.toggleDebug();
        });
    }

    function synchronizeAutomaticExpandCheckboxes() {
        $('automaticExpandTop').observe('change', function (e) {
            $('automaticExpandBottom').checked = $('automaticExpandTop').checked;
        });
        $('automaticExpandBottom').observe('change', function (e) {
            $('automaticExpandTop').checked = $('automaticExpandBottom').checked;
        });
    }

    function observeSearch() {
        function startSearch(searchText) {
            if (!searchText) {
                return;
            }

            new Ajax.Request(URL_SEARCH_LOG_RECORDS, {
                parameters: {
                    'search-text': searchText
                },
                onSuccess: function (response) {
                    var parentRecordIds = response.responseText.evalJSON();
                    logRecords.expandRecordsByIds(parentRecordIds);
                }
            });
        }

        $('buttonSearch').observe('click', function() {
            var searchText = $('searchText').value;
            logRecords.setHighlightText(searchText);
            logRecords.setFilterText('');
            startSearch(searchText);
        });
        $('buttonFilter').observe('click', function() {
            var searchText = $('searchText').value;
            logRecords.setHighlightText(searchText);
            logRecords.setFilterText(searchText);
            startSearch(searchText);
        });
    }

    function highlight(text, search) {
        if (!search) {
            return text.escapeHTML();
        }

        var startIndex = text.toLowerCase().indexOf(search.toLowerCase());
        if (startIndex == -1) {
            return text.escapeHTML();
        } else {
            return (
                text.substr(0, startIndex).escapeHTML() +
                '<span class="plesk-migrator-log-search-highlight">' +
                text.substr(startIndex, search.length).escapeHTML() +
                '</span>' +
                highlight(text.substr(startIndex + search.length), search)
            );
        }
    }

    var logRecords = new LogRecords();
    logRecords.appendRecords(INITIAL_LOG_RECORDS);

    synchronizeScrollCheckboxes();
    observeDebugCheckboxes();
    synchronizeAutomaticExpandCheckboxes();
    observeSearch();
    periodicUpdateLog();
});